chore: update gitignore and add root docs and tooling

This commit is contained in:
2026-05-21 16:05:53 +07:00
parent c72dde4484
commit 2311767e9f
12 changed files with 2582 additions and 0 deletions
+4
View File
@@ -34,3 +34,7 @@ yarn-error.log*
!/storage/framework/testing/.gitignore
!/storage/framework/phpstan/.gitignore
!/storage/logs/.gitignore
# Subproject ignores
/Project/storage/
/Project/**/*.log
+153
View File
@@ -0,0 +1,153 @@
# Changelog
All notable changes to this project. Format inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and **Semantic Versioning**.
## [Unreleased] — `advanced` branch
### Real-time Dashboard & Custom Widgets (2026-05-16)
#### Added
- **`DashboardStatsUpdated` event** (`app/Events/DashboardStatsUpdated.php`) — implements `ShouldBroadcast`, broadcasts slim stats payload (cpu, ram, disk, users, queues, uptime) to the `admin.monitoring` private channel as `stats.updated`. Replaces the previous `setInterval` polling loop.
- **`BroadcastDashboardStats` artisan command** (`app/Console/Commands/BroadcastDashboardStats.php`) — signature: `dashboard:broadcast-stats`. Clears `monitoring_full_bundle` cache, calls `SystemMonitoringService::getAll()`, dispatches `DashboardStatsUpdated`. Scheduled every minute with `withoutOverlapping()`.
- **Dashboard real-time listener** — `window.Echo.private('admin.monitoring').listen('.stats.updated', applyStats)` in `dashboard.blade.php`. Falls back to `setInterval(refreshStats, 30000)` only when Reverb is not connected.
- **`dashboard_widget_preferences` table** (migration `2026_05_16_220000_create_dashboard_widget_preferences_table.php`) — columns: `user_id` (FK → users, cascade delete), `widget_key` (varchar 64), `visible` (boolean), `sort_order` (smallint). Unique constraint on `(user_id, widget_key)`.
- **`DashboardWidgetPreference` model** (`app/Models/DashboardWidgetPreference.php`) — `forUser(int $userId)` merges static defaults with saved DB prefs, sorted by `sort_order`. `defaults()` defines 7 widgets: `cpu`, `ram`, `disk`, `live_users`, `queues`, `activity_feed`, `ai_insight`.
- **Widget partials** in `resources/views/pages/dashboard/`:
- `widget-cpu.blade.php`, `widget-ram.blade.php`, `widget-disk.blade.php`
- `widget-live-users.blade.php`, `widget-queues.blade.php`
- `widget-quick-actions.blade.php` (quick nav links to common admin pages)
- **Dashboard Customize panel** — floating "Customize" button reveals a slide-down panel with per-widget toggle switches, drag-to-reorder via **SortableJS**, Save Layout button (POSTs to `dashboard.widgets.save`), and Reset to Default button.
- **`DashboardController@saveWidgetPreferences`** — validates and upserts widget prefs (key, visible, sort_order) per user.
- **`DashboardController@resetWidgetPreferences`** — deletes all prefs for authenticated user, restoring defaults.
- **Route** `POST /dashboard/widgets``DashboardController@saveWidgetPreferences` named `dashboard.widgets.save`.
- **SortableJS** loaded via CDN in `dashboard.blade.php` for drag-to-reorder widget grid.
---
### Granular Tab Permission System (2026-05-16)
#### Added
- **85 granular tab-level permissions** covering every settings tab in Global Settings and Mobile Settings panels (e.g., `view global settings branding`, `manage global settings password policy`, `view mobile settings kill switch`).
- **`CheckTabPermission` middleware** — enforces tab-level access control when navigating to specific settings tabs.
- **`@cantab` / `@managetab` Blade directives** — shorthand for checking tab-level permissions in views.
- **Tree-structured UI in role modals** — permission list in Create/Edit Role modals shows permissions grouped in a collapsible tree, making it easier to assign tab-level permissions.
- **Two-panel drag-and-drop permission picker** — role modals have an Available panel and an Assigned panel; items can be dragged between panels or double-clicked to move. Both panels display **category group headers** that appear/disappear dynamically as items move between panels. Multi-select supported.
---
### UI & UX Improvements (2026-05-16)
#### Added
- **Modal border-radius** — `.modal-content` globally styled with `border-radius: 20px` and `overflow: clip` (clip prevents scroll container creation while still clipping visually).
- **Modal width** — `modal-xl` and `modal-permission` modals set to `calc(70vw - 40px)` globally via `app.blade.php`.
#### Fixed
- **Modal scroll broken** — `overflow: hidden` was replaced with `overflow: clip`. `clip` clips visually without creating a new scroll container, so scrollable children inside modals work correctly.
- **Sidebar submenu cannot close** — replaced Alpine.js approach (which was intercepted by the AdminUIUX theme) with vanilla JS `initSidebarSubmenus()` in `navigation.blade.php`. Uses `data-sidebar-toggle` attribute pattern, `e.stopPropagation()`, and `cloneNode()` to prevent duplicate event listeners. Chevron rotates 180° on open via inline style transform, not Alpine transitions.
---
### Tests (+84, total 371 passing) (2026-05-16)
- Added test coverage for tab permission middleware, widget preference CRUD, and real-time broadcast event.
---
### Data Retention Wave (2026-05-15)
#### Added
- **`OtpCode` — Prunable trait**: OTP code yang sudah melewati `expires_at` dipangkas otomatis setiap kali `model:prune` berjalan. Sebelumnya record expired menumpuk tanpa batas.
- **`UserTrustedDevice` — Prunable trait**: Device entry yang sudah melewati `expires_at` dipangkas otomatis. Konsisten dengan OtpCode — keduanya prune berdasarkan kolom `expires_at` yang sudah ada.
- **`AiHealingLog` — Prunable trait, retensi 90 hari**: Log AI self-healing sebelumnya tidak memiliki retention policy. Sekarang dipangkas setelah 90 hari (`created_at`), sejajar dengan `ai_usage_logs` dan `mobile_error_logs`.
- **`PasswordHistory` — Prunable trait, retensi 365 hari**: History password disimpan untuk N-reuse check. Record di luar 365 hari tidak lagi relevan untuk kebijakan reuse dan kini dipangkas otomatis.
- **`telescope:prune --hours=48`** dijadwalkan harian pukul 03:05: Telescope debugging data sebelumnya tumbuh tanpa batas di production. Entry lebih dari 48 jam dipangkas setelah prune model selesai.
#### Summary Retention Matrix (setelah wave ini)
| Model / Table | Retensi | Basis |
|---|---|---|
| `otp_codes` | Setelah expired | `expires_at < now()` |
| `user_trusted_devices` | Setelah expired | `expires_at < now()` |
| `ai_healing_logs` | 90 hari | `created_at` |
| `password_histories` | 365 hari | `created_at` |
| `mobile_error_logs` | 90 hari | `occurred_at` |
| `ai_usage_logs` | 90 hari | `created_at` |
| `mobile_sync_logs` | 30 hari | `synced_at` |
| `notifications` | 30 hari | `created_at` |
| `activity_log` | 365 hari | Spatie config |
| `telescope_entries` | 48 jam | `telescope:prune` |
---
### Added
#### Tests (+287, subtotal 287 passing)
- **Auth security** — `TwoFactorTest` (10), `SocialAuthTest` (9), `ImpersonateTest` (9), `WebAuthnConfigTest` (6).
- **Authorization & gating** — `RoleManagementTest` (13), `PermissionManagementTest` (10), `UserManagementTest` (14), `CheckActivePermissionTest` (4), `IpAccessControlTest` (8), `CheckLegalAgreementTest` (4), `PasswordExpiryMiddlewareTest` (4), `SecurityHeadersTest` (6).
- **Service layer** — `SystemConfigServiceTest` (15), `PasswordPolicyServiceTest` (9), `BackupManagementServiceTest` (7).
- **Pure unit** (no Laravel boot) — `SettingValueCasterTest` (23), `ActivityFormatterTest` (22), `MonitoringFormatterTest` (14), `SessionHelperTest` (12), `PasswordRuleHelperTest` (8), `ApiResponseTest` (10), `CustomExceptionsTest` (4).
- **Database integrity** — `CascadeIntegrityTest` (9) locking FK + soft-delete contracts.
- **Performance** — `NPlusOneTest` (3) regression locks for datatables.
- **Rate limiting** — `RateLimitTest` (6) covering login/register/forgot-password/OTP/2FA + per-IP isolation.
- **API contracts** — `HealthTest` rewrites, `MobileConfigTest` rewrites (ETag + 304).
#### Schema
- `2026_05_12_120000_add_social_columns_to_users_table``google_id`, `facebook_id`, `github_id` columns (previously referenced in `User::$fillable` with no migration).
- `2026_05_14_100000_add_performance_indexes` — composite/secondary indexes:
- `password_histories(user_id, created_at)`
- `system_setting_revisions(key, created_at)` and `(changed_by)`
- `notifications(notifiable_type, notifiable_id, read_at)`
- `2026_05_14_110000_add_fk_to_audit_columns` — FK constraints for `roles.{created_by,updated_by}` and `permissions.{created_by,updated_by}` with `ON DELETE SET NULL`. Orphan ids are nulled before constraint creation.
#### Code
- `App\Services\Monitoring\MonitoringFormatter` — extracted from `SystemMonitoringService`. Methods: `bytes()`, `parseBytes()`, `duration()`.
- `App\Services\SystemConfig\{SettingDefinitions,SettingValueCaster,SettingFileUploader}` — extracted from a 1.272-line god class.
- `App\Exceptions\SystemConfigException` — factories: `unknownKey()`, `imageUploadFailed()`.
- `App\Exceptions\BackupOperationException` — factories: `missingBinary()`, `diskNotConfigured()`, `restoreFailed()`.
- `App\Exceptions\MonitoringException` — factories: `unsupportedOs()`, `probeFailed()`.
#### Tooling & Docs
- **Larastan level 5** with baseline (`phpstan.neon` + `phpstan-baseline.neon`). 82 pre-existing findings are silenced; new code must stay clean.
- **Laravel Pint** applied across the codebase (267 files PSR-12 compliant).
- **CI workflow** (`.github/workflows/ci.yml`) — 3 jobs: `test`, `lint` (pint + composer audit + permissions:audit), `static-analysis` (Larastan).
- **`SECURITY.md`** — vulnerability reporting + supply-chain advisory record + defense-in-depth overview.
- **`CHANGELOG.md`** — this file.
- All major Markdown docs refreshed (`README.md`, `TECH_STACK.md`, `USER_GUIDE.md`, `DEPLOYMENT_GUIDE.md`, `mobile/README.md`).
### Changed
- `SystemConfigService`**1.272 → 197 lines** (-85%). Public API preserved.
- `BackupManagementService` — private `parseBytes`/`formatBytes` delegate to `MonitoringFormatter`. Single canonical implementation.
- `routes/web.php``/auth/callback` now declared before `/auth/{provider}` so OAuth callback is reachable. `{provider}` constrained to `google|facebook|github`.
- `app/Http/Controllers/Api/HealthController` — returns `503` only when at least one check has `status=fail`; `warn` keeps `200` with `status=warn`. Matches OpenAPI annotation.
- `declare(strict_types=1)` on core utility classes (`MonitoringFormatter`, `SettingValueCaster`, `SettingFileUploader`, `ApiResponse`, all new exceptions).
### Fixed
- **`SystemConfigService` stale static cache** — `update()` did not reset `$resolvedSettings`, causing a second update in the same request to compare against pre-update values and silently skip the write. Fixed by always nulling the static in `invalidateCache()`.
- **OAuth callback unreachable** — `/auth/{provider}` ordered before `/auth/callback` caused the wildcard to swallow `/auth/callback` and resolve it as `redirect('callback')`, which 404s on the missing feature flag. Fixed by reordering routes.
- **OAuth identity-overwrite** — callback fell through to `User::create()` on email collision with a different provider id, throwing a unique-violation 500. Fixed by explicitly refusing the link and redirecting to `/login` with a generic error (avoids account enumeration).
- **Missing migration for OAuth columns** — `users.{google_id,facebook_id,github_id}` were in `$fillable` but no migration created them. Added.
- **`HealthController` 503 false-alarm** — `every($checks, status === 'ok')` treated storage `warn` (>90% disk) as unhealthy. Now: only `fail` triggers 503.
- **Pest flake in `UserManagementTest`** — `fake()->name()` could produce strings outside the controller's `/^[a-zA-Z\s]+$/` regex, intermittently failing the update tests under full-suite runs. Hardcoded display names.
### Security
- `SecurityHeaders` middleware (wired globally) — `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, `X-XSS-Protection`, HSTS (HTTPS + opt-in).
- Test coverage for the entire auth boundary: 2FA, Impersonate, SocialAuth (incl. identity-overwrite protection), 4 middleware (`IpAccessControl`, `CheckActivePermission`, `PasswordExpiry`, `CheckLegalAgreement`).
- Rate-limit regression tests prevent silent throttle removal.
### Known Follow-ups
- `SystemMonitoringService` (~600 lines, `shell_exec`/`disk_*`/`sys_getloadavg`) still tightly coupled to OS calls. Refactor target: extract `SystemInfoProvider` interface + `LinuxSystemInfoProvider`/`WindowsSystemInfoProvider`/`FakeSystemInfoProvider` so the service can be unit-tested without mocking globals. Deferred to a dedicated change.
- `laragear/webauthn` is marked **abandoned** upstream. Replacement: `laravel/passkeys`. Tracked separately because the public surface differs.
- Mobile `expo`/`@expo/cli`/`@expo/metro-config`/`postcss` chain has **4 moderate** CVEs reachable only in dev-build tooling. Fix requires Expo SDK bump (breaking change).
+478
View File
@@ -0,0 +1,478 @@
# Deployment Guide
Panduan instalasi aplikasi di server produksi **Ubuntu 22.04+**.
---
## Spesifikasi Server
| Komponen | Minimum | Rekomendasi |
|----------|---------|-------------|
| CPU | 2 core | 4 core |
| RAM | 4 GB | 8 GB |
| Storage | 40 GB SSD | 100 GB SSD |
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
---
## Langkah 1 — Install Dependensi
```bash
sudo apt update && sudo apt upgrade -y
# PHP 8.3 + ekstensi
sudo add-apt-repository ppa:ondrej/php -y
sudo apt install -y php8.3 php8.3-fpm php8.3-pgsql php8.3-redis \
php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip php8.3-gd \
php8.3-bcmath php8.3-intl php8.3-readline
# PostgreSQL, Redis, Nginx, Supervisor
sudo apt install -y postgresql postgresql-contrib redis-server nginx supervisor
sudo systemctl enable redis-server
# Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
```
---
## Langkah 2 — Setup Database
```bash
sudo -u postgres psql
```
```sql
CREATE USER biiproject WITH PASSWORD 'password_kuat_anda';
CREATE DATABASE biiproject_db OWNER biiproject;
GRANT ALL PRIVILEGES ON DATABASE biiproject_db TO biiproject;
\q
```
---
## Langkah 3 — Deploy Aplikasi
```bash
cd /var/www
sudo git clone <repo-url> html
sudo chown -R $USER:www-data html
cd html
composer install --no-dev --optimize-autoloader
npm install && npm run build
cp .env.example .env
php artisan key:generate
```
### Edit `.env`
```env
APP_NAME="biiproject"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://domain.com
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=biiproject_db
DB_USERNAME=biiproject
DB_PASSWORD=password_kuat_anda
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
BROADCAST_CONNECTION=reverb
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=domain.com
REVERB_PORT=8080
REVERB_SCHEME=https
BCRYPT_ROUNDS=12
# Opsional — Error monitoring
SENTRY_LARAVEL_DSN=https://xxxx@sentry.io/xxxx
SENTRY_TRACES_SAMPLE_RATE=0.1
SENTRY_PROFILES_SAMPLE_RATE=0.1
```
### Migrasi, Seed & Optimisasi
```bash
php artisan migrate --force
php artisan db:seed --force
php artisan cache:clear # wajib setelah seeder
php artisan storage:link
php artisan optimize
php artisan l5-swagger:generate # regen API docs
```
### Post-Deploy Verification
```bash
# Quality gate — pastikan tidak ada regresi
./vendor/bin/phpstan analyse --memory-limit=1G
./vendor/bin/pint --test
composer audit --no-dev
# Health check
curl -sf https://domain.com/api/health | jq .
# expect: {"status":"healthy","timestamp":"...","checks":{...}}
```
---
## Langkah 4 — Konfigurasi Nginx
`/etc/nginx/sites-available/biiproject`:
```nginx
server {
listen 80;
server_name domain.com;
root /var/www/html/public;
index index.php;
client_max_body_size 50M;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
# WebSocket Reverb
location /app {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 60s;
}
location ~ /\.(?!well-known) { deny all; }
}
```
> **Catatan keamanan:** Laravel mengirim header `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, dan `Permissions-Policy` dari middleware `SecurityHeaders`. Tidak perlu duplikasi di Nginx — biarkan aplikasi yang kontrol (lebih mudah di-update via setting).
```bash
sudo ln -s /etc/nginx/sites-available/biiproject /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
---
## Langkah 5 — SSL (Let's Encrypt)
```bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d domain.com
```
Setelah HTTPS aktif, **aktifkan HSTS** dari admin panel: Global Settings → Login Security → `HSTS Enabled`. Aplikasi akan mengirim header `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`.
---
## Langkah 6 — Background Workers (Supervisor)
### Queue Worker
`/etc/supervisor/conf.d/biiproject-worker.conf`:
```ini
[program:biiproject-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
```
### Reverb (WebSocket)
`/etc/supervisor/conf.d/biiproject-reverb.conf`:
```ini
[program:biiproject-reverb]
command=php /var/www/html/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/reverb.log
```
### Scheduler (Cron)
```bash
echo "* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1" \
| sudo crontab -u www-data -
```
> Scheduler menjalankan `dashboard:broadcast-stats` setiap menit — membutuhkan queue worker dan Reverb **sudah berjalan** agar broadcast terkirim ke browser. Pastikan keduanya distart via Supervisor sebelum mengaktifkan cron.
### Aktifkan
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all
```
---
## Langkah 7 — Permission File
```bash
sudo chown -R www-data:www-data /var/www/html
sudo chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache
```
---
## Langkah 8 — Konfigurasi Pasca-Deploy
### A. Global Settings
Login sebagai Super Admin → System Settings → Global Settings. Pastikan:
- [ ] `Password Policy`: min 12, charset mixed-case + digit + symbol, expiry & history sesuai kebijakan
- [ ] `Login Security`: max attempts 5, lockout duration, 2FA toggle, captcha jika perlu
- [ ] `IP & Access`: whitelist admin (IP kantor/VPN), auto-block on burst
- [ ] `HSTS Enabled`: ON (setelah SSL aktif)
- [ ] `Single Session`: ON jika kebijakan satu-device-per-akun
### B. Verifikasi Quality Gate
```bash
# Pastikan dependencies aman
composer audit --no-dev --abandoned=ignore
# Pastikan tidak ada regresi
php artisan test --parallel
```
---
## Layanan Eksternal (Opsional)
Sebagian besar bisa diatur via **Global Settings** di admin panel — tidak perlu edit `.env`.
### Email SMTP
```env
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_USERNAME=postmaster@domain.com
MAIL_PASSWORD=xxxxxxxx
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@domain.com
MAIL_FROM_NAME="biiproject"
```
### Sentry (Error Monitoring)
```env
SENTRY_LARAVEL_DSN=https://xxxx@o0.ingest.sentry.io/xxxx
SENTRY_TRACES_SAMPLE_RATE=0.1
```
Buat project di [sentry.io](https://sentry.io), pilih platform **Laravel**, dan salin DSN-nya.
### Telegram Bot
1. Chat `@BotFather``/newbot` → simpan **Token**
2. Kirim pesan ke bot, buka `https://api.telegram.org/bot<TOKEN>/getUpdates` → simpan **Chat ID**
3. Masukkan via Global Settings → Notifications
> **Catatan:** `IpAccessControl` middleware mengirim alert otomatis ke Telegram saat sebuah IP di-auto-block karena burst — pastikan token + chat id valid.
### Google Drive Backup
1. Google Cloud Console → buat project → enable **Google Drive API**
2. Buat **OAuth 2.0 Client ID** (Desktop app)
3. Dapatkan **Refresh Token** via OAuth Playground
4. Masukkan Client ID, Client Secret, Refresh Token, dan nama folder via Global Settings → Backup
### Amazon S3
1. Buat IAM user dengan policy `AmazonS3FullAccess`
2. Buat S3 bucket
3. Masukkan Access Key, Secret, Bucket, Region (dan endpoint opsional) via Global Settings → Backup
### Firebase Cloud Messaging (Push Notification Mobile)
1. Buat project di Firebase Console → Project Settings → Cloud Messaging
2. Unduh `google-services.json` (Android) atau `GoogleService-Info.plist` (iOS)
3. Letakkan di direktori mobile app sesuai panduan Expo
4. Device token diregistrasikan otomatis via API `/api/v1/devices/register`
---
## CI/CD
Workflow di `.github/workflows/ci.yml`. Setiap push ke `main`/`develop`/`config`/`advanced` dan setiap PR ke `main`/`develop` menjalankan:
1. **Test**`php artisan test --parallel` (Postgres 15 + Redis 7 service containers)
2. **Lint**`pint --test` + `composer audit --abandoned=ignore` + `permissions:audit`
3. **Static Analysis**`phpstan analyse --memory-limit=1G`
Branch protection: PR tidak bisa di-merge kalau salah satu job gagal.
---
## Update Aplikasi
Cara tercepat dan teraman untuk memperbarui aplikasi adalah menggunakan skrip deployment otomatis yang sudah disediakan:
```bash
cd /var/www/html
sudo chmod +x deploy.sh
./deploy.sh
```
Skrip ini akan melakukan:
1. Masuk ke Maintenance Mode.
2. Update dependencies (Composer & NPM).
3. Build assets (Vite).
4. Run migrations.
5. Optimasi cache & pembersihan log.
6. Restart background workers (Queue & Reverb).
7. Verifikasi kesehatan sistem.
8. Keluar dari Maintenance Mode.
### Update Manual
```bash
cd /var/www/html
git pull origin main
php artisan down --secret="your-bypass-key"
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan l5-swagger:generate
sudo supervisorctl restart biiproject-worker:*
sudo supervisorctl restart biiproject-reverb
# Verifikasi
curl -sf https://domain.com/api/health | jq -e '.checks.database.status == "ok"'
php artisan up
```
---
## Deployment via Docker (alternatif)
```bash
git clone <repo-url> Project
cd Project
cp .env.example .env
# Edit .env:
# DB_HOST=pgsql (nama service Docker, bukan 127.0.0.1)
# REDIS_HOST=redis
./vendor/bin/sail up -d
./vendor/bin/sail artisan migrate --seed --force
./vendor/bin/sail artisan cache:clear
./vendor/bin/sail artisan optimize
```
> **Penting:** Semua perintah `php artisan` di lingkungan Docker **harus** dijalankan via `./vendor/bin/sail artisan`, bukan langsung. Host `pgsql` hanya bisa di-resolve dari dalam Docker network.
---
## Troubleshooting
| Masalah | Solusi |
|---------|--------|
| 502 Bad Gateway | `sudo systemctl status php8.3-fpm` |
| Queue tidak jalan | `sudo supervisorctl status` |
| Permission denied | `sudo chown -R www-data:www-data storage bootstrap/cache` |
| Cache error setelah update | `php artisan optimize:clear && php artisan optimize` |
| Settings tidak muncul setelah seeder | `php artisan cache:clear` (settings di-cache 60 menit) |
| WebSocket gagal connect | `sudo supervisorctl status biiproject-reverb` + cek port 8080 di firewall |
| `pgsql` host tidak resolve | Pastikan `DB_HOST=127.0.0.1` (bukan `pgsql`) di server non-Docker |
| Telescope/log tidak ditemukan | Cek `storage/logs/laravel.log` & menu System Monitoring → Logs |
| Error monitoring tidak aktif | Pastikan `SENTRY_LARAVEL_DSN` sudah diset di `.env` |
| `/api/health` return 503 | Cek setiap field `checks.*.status` — yang `fail` mengindikasikan masalah. `warn` (storage >90%) tetap 200 by design. |
| OAuth callback 404 | Pastikan route order: `/auth/callback` HARUS sebelum `/auth/{provider}` di `routes/web.php`. |
| `column "google_id" does not exist` | Pastikan migrasi `2026_05_12_120000_add_social_columns_to_users_table` sudah jalan. |
| Dashboard stats tidak update real-time | Cek Reverb berjalan (`supervisorctl status biiproject-reverb`), queue worker aktif, dan `BROADCAST_CONNECTION=reverb` di `.env`. |
| Dashboard widget tidak tersimpan | Pastikan migrasi `2026_05_16_220000_create_dashboard_widget_preferences_table` sudah dijalankan. |
---
## Performance Tuning
### PHP-FPM
`/etc/php/8.3/fpm/pool.d/www.conf`:
```ini
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
```
### OPcache
`/etc/php/8.3/fpm/php.ini`:
```ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; production only
opcache.preload=/var/www/html/bootstrap/cache/preload.php
opcache.preload_user=www-data
```
### Redis Tuning
`/etc/redis/redis.conf`:
```
maxmemory 1gb
maxmemory-policy allkeys-lru
```
### PostgreSQL
Tambah index recommendation: sudah ada (lihat migrasi `2026_05_14_100000_add_performance_indexes`). Untuk dataset besar (>10M rows pada `activity_log`), pertimbangkan `pg_partman` untuk partisi bulanan.
+127
View File
@@ -0,0 +1,127 @@
# Security
## Reporting
Report vulnerabilities to **andikadebiputra@gmail.com**. Please do not open public issues for security problems.
We aim to acknowledge within 48 hours and to ship a fix or mitigation for critical issues within 7 days.
---
## Supply Chain — Known Advisories (as of 2026-05-15)
### Backend (`composer audit`)
- **No vulnerability advisories.**
- `laragear/webauthn` is marked **abandoned**. Replacement: `laravel/passkeys`. Migration planned for a separate change since the public surface differs.
### Frontend root (`npm audit`)
- **No vulnerabilities.**
### Mobile (`mobile/`, `npm audit`)
- **4 moderate** — `postcss < 8.5.10` ([GHSA-qx2v-qp2m-jg93](https://github.com/advisories/GHSA-qx2v-qp2m-jg93), XSS via unescaped `</style>` in CSS output).
- Path: transitively pulled by `expo``@expo/cli``@expo/metro-config``postcss`.
- Reachability: the vulnerable code path is in dev build tooling, not the runtime bundle shipped to devices. End-user impact is **low**.
- Fix requires bumping Expo SDK (breaking change). Tracked separately.
CI runs `composer audit --abandoned=ignore` on every push and will fail on new vulnerabilities.
---
## Defense in Depth
### Authentication
- **2FA** — email OTP (6 digit) with optional **trust-device** cookie (UUID + secret hashed, 30 days). Verify endpoint rate-limited 5/min.
- **Passkey (WebAuthn)** — via `laragear/webauthn`, FIDO2-compliant. Login flow is challenge-bound.
- **Social OAuth** — Google, Facebook, GitHub via Socialite. Callback explicitly refuses identity-overwrite when an email is already linked to a different provider id.
- **Captcha** — Google reCAPTCHA v2/v3 toggleable per environment.
### Authorization
- Spatie `permission`/`role` system with `active-permission` gate — a permission can be disabled centrally without revoking it from each role.
- **Granular tab permissions** — 85 named permissions covering every settings tab in Global Settings and Mobile Settings. `CheckTabPermission` middleware enforces access; `@cantab`/`@managetab` Blade directives available for views.
- **Impersonation** guarded against: self-impersonate, Developer role, inactive users, and nested loop. Tracked in `Cache` for the target user awareness banner and audit logged via `ImpersonationStatusChanged` event.
### Network & Boundary
- **IP access control** — `IpAccessControl` middleware with:
- Global blacklist
- Admin route whitelist (`/users*`, `/roles*`, `/permissions*`, `/system-config*`, `/backups*`, `/admin/*`)
- Auto-block IPs that exceed configurable burst threshold (24h cooldown). Sends Telegram firewall alert.
- HSTS toggle (HTTPS only).
- **Security headers** — `SecurityHeaders` middleware sets `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()`, `X-XSS-Protection`, and (when HTTPS + opt-in) `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`.
- **Rate limit** — Per-endpoint throttle: `/login` (5/min), `/2fa verify` (5/min), `/forgot-password`, `/api/v1/login` (10/min), `/api/v1/register` (5/min), `/api/v1/otp/send` (5/min), `/api/v1/otp/verify` (10/min). Per-IP buckets isolated.
- **Single-session enforcement** — optional; logs out previous device when user logs in elsewhere.
### Passwords & Sessions
- **Bcrypt 12 rounds** in production (4 in test for speed).
- **Password policy** (`App\Services\Auth\PasswordPolicyService`): min/max length, mixed-case, digit, symbol, expiry, history reuse blocker (configurable N).
- **Password history** stored hashed in `password_histories`; reuse of last N raises validation error.
- **Sessions** stored in Redis. Admins can revoke sessions per user from `/system-settings/session-manager`.
### Data Integrity
- **Foreign keys** — all audit (`created_by`/`updated_by`) → `users(id) ON DELETE SET NULL`. Owned data (`password_histories`, `user_consents`, `user_trusted_devices`, `model_has_*`, `role_has_permissions`) → `ON DELETE CASCADE`. Cascade behavior is locked in by `tests/Feature/Database/CascadeIntegrityTest.php`.
- **Composite indexes** on hot lookups (`password_histories(user_id, created_at)`, `system_setting_revisions(key, created_at)`, `notifications(notifiable, read_at)`).
- **Soft deletes** — User, Role, Permission. Restore + force-delete flows tested.
### Data Retention
Semua data time-sensitive memiliki kebijakan retensi otomatis yang dijalankan oleh scheduler harian:
| Tabel | Retensi | Mekanisme |
|-------|---------|-----------|
| `otp_codes` | Setelah expired | `OtpCode::prunable()``expires_at < now()` |
| `user_trusted_devices` | Setelah expired | `UserTrustedDevice::prunable()``expires_at < now()` |
| `ai_healing_logs` | 90 hari | `AiHealingLog::prunable()``created_at` |
| `password_histories` | 365 hari | `PasswordHistory::prunable()``created_at` |
| `mobile_error_logs` | 90 hari | `MobileErrorLog::prunable()``occurred_at` |
| `ai_usage_logs` | 90 hari | `AiUsageLog::prunable()``created_at` |
| `mobile_sync_logs` | 30 hari | `MobileSyncLog::prunable()``synced_at` |
| `notifications` | 30 hari | `Notification::prunable()``created_at` |
| `activity_log` | 365 hari | `activitylog:clean` via Spatie config |
| `telescope_entries` | 48 jam | `telescope:prune --hours=48` |
| `dashboard_widget_preferences` | Dihapus saat user dihapus | FK `ON DELETE CASCADE` ke `users` |
Scheduler berjalan: `model:prune` pukul 03:00, `telescope:prune` pukul 03:05, `activitylog:clean` harian. Sesuai prinsip **data minimization** UU PDP No. 27/2022.
### Audit & Forensics
- **Spatie ActivityLog** on User, Role, Permission, SystemSetting.
- **System config revisions** — separate `system_setting_revisions` table captures actor, IP, user agent on every change.
- **`ActivityFormatter`** redacts sensitive keys (`password`, `remember_token`, `secret`, `key`, `token`, `2fa_secret`) from displayed diffs.
### Error Handling & Monitoring
- **Custom exception classes** — `App\Exceptions\{SystemConfig,BackupOperation,Monitoring}Exception` with factory methods, allowing specific handlers rather than generic `\Exception` catches.
- **Sentry** — production error reporting; sample rates set in `.env`.
- **Health endpoint** — `GET /api/health` reports DB, Redis, storage, queue; returns `503` only when any check `fail`s, `200` for `warn` (e.g. disk >90%).
---
## Quality Gate
| Check | Tool | Threshold |
|-------|------|-----------|
| Unit + feature tests | Pest 4 | All passing |
| Static analysis | Larastan level 5 + baseline | No new errors |
| Code style | Laravel Pint | All files PSR-12 compliant |
| Dependency audit | `composer audit` | 0 vulnerabilities |
| N+1 regression | Pest (Query Log) | Bounded query count |
CI workflow: `.github/workflows/ci.yml`.
---
## Disclosure Timeline (template)
```
T+0 Report received
T+48h Acknowledgement sent
T+7d Patch landed (critical) / ETA shared (others)
T+30d Coordinated disclosure window ends
```
+327
View File
@@ -0,0 +1,327 @@
# Tech Stack
Daftar lengkap teknologi yang dipakai di proyek ini, beserta penjelasan singkat kegunaannya.
---
## 1. Runtime & Bahasa
| Teknologi | Versi | Kegunaan |
|-----------|-------|----------|
| **PHP** | 8.2+ | Bahasa utama backend. Kelas utility pakai `declare(strict_types=1)`. |
| **Node.js** | 20+ | Build asset frontend (Vite) + tooling mobile |
| **PostgreSQL** | 15+ | Database relasional utama (ACID-compliant). Skema pakai FK + cascade penuh. |
| **Redis** | 7.x | Cache, session store, queue, broadcast driver |
---
## 2. Framework Inti
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `laravel/framework` | ^13.0 | Framework PHP utama (routing, ORM, middleware, dll) |
| `laravel/sanctum` | ^4.0 | Autentikasi API berbasis token untuk mobile app |
| `laravel/socialite` | ^5.24 | OAuth login (Google, Facebook, GitHub) |
| `laravel/reverb` | ^1.10 | WebSocket server native untuk notifikasi real-time |
| `laravel/pulse` | ^1.7 | Monitoring performa app (request, queue, cache, slow queries) |
| `laravel/horizon` | ^5.46 | Queue dashboard (Redis-backed) |
| `laravel/breeze` | ^2.3 | Scaffolding autentikasi (login, register, reset password) |
| `laravel/tinker` | ^3.0 | REPL interaktif untuk debugging via terminal |
---
## 3. Database & Storage
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `predis/predis` | ^3.4 | Client PHP untuk Redis (PSR-compliant) |
| `masbug/flysystem-google-drive-ext` | ^2.5 | Driver Flysystem untuk Google Drive (backup) |
> Driver S3 sudah built-in di Laravel — cukup set `FILESYSTEM_DISK=s3` di `.env`.
### Skema database
- 40+ tabel, semua bermigrasi (lihat `database/migrations/`).
- FK constraint penuh: audit `created_by`/`updated_by``users(id) ON DELETE SET NULL`; data milik user → `ON DELETE CASCADE` (lihat `2026_05_14_110000_add_fk_to_audit_columns.php`).
- Composite indexes pada tabel hot (`password_histories`, `system_setting_revisions`, `notifications`) — lihat `2026_05_14_100000_add_performance_indexes.php`.
- **Data retention otomatis** via Laravel `Prunable` trait pada 8 model + `telescope:prune` + `activitylog:clean`. Retention policy lengkap ada di `SECURITY.md`.
---
## 4. Autentikasi & Keamanan
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `laragear/webauthn` | ^5.0 | Passkey / biometric login (FIDO2/WebAuthn) — ⚠️ marked abandoned upstream; replacement: `laravel/passkeys` |
| `anhskohbo/no-captcha` | ^3.7 | Integrasi Google reCAPTCHA v2/v3 di form login |
### Built-in (no extra package)
- **2FA via email OTP** + trust-device cookie (file: `app/Http/Controllers/Auth/TwoFactorController.php`)
- **Password policy** — `App\Services\Auth\PasswordPolicyService` (min/max/charset/expiry/history-reuse-block)
- **IP access control** — `app/Http/Middleware/IpAccessControl.php` (blacklist, admin whitelist, auto-block on burst, HSTS toggle)
- **Security headers** — `app/Http/Middleware/SecurityHeaders.php` (X-Frame, X-CTO, Referrer, Permissions-Policy, X-XSS, HSTS)
- **Session manager** — list & force-logout active sessions
- **Impersonate** — `ImpersonateController` dengan guard self/Developer/inactive + loop prevention
- **Single-session enforcement** opsional (di-toggle dari Global Settings)
---
## 5. Manajemen Hak Akses & Audit (Spatie)
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `spatie/laravel-permission` | ^6.24 | Sistem role & permission granular |
| `spatie/laravel-activitylog` | ^4.10 | Audit trail — mencatat perubahan data |
| `spatie/laravel-backup` | ^10.2 | Backup database & file ke Local/S3/GDrive |
| `spatie/laravel-medialibrary` | ^11.21 | Upload & manajemen file media (avatar, dokumen) |
---
## 5b. Dashboard Widget System
Per-user persisted widget layout. Architecture:
| Layer | Class / File | Fungsi |
|-------|-------------|--------|
| Model | `DashboardWidgetPreference` | `forUser()` merge defaults + DB prefs, sorted by `sort_order` |
| Migration | `2026_05_16_220000_create_dashboard_widget_preferences_table` | `user_id` FK cascade, unique `(user_id, widget_key)` |
| Controller | `DashboardController@saveWidgetPreferences` | upsert prefs via `updateOrCreate` |
| Controller | `DashboardController@resetWidgetPreferences` | delete all prefs → restore defaults |
| Route | `POST /dashboard/widgets` (`dashboard.widgets.save`) | — |
| Partials | `resources/views/pages/dashboard/widget-*.blade.php` | cpu, ram, disk, live-users, queues, quick-actions |
| JS | SortableJS (CDN) | drag-to-reorder grid |
| Broadcasting | `DashboardStatsUpdated` event → Reverb → Echo | push stats every minute via `dashboard:broadcast-stats` |
### Sidebar Toggle
Sidebar submenus use **vanilla JS** `initSidebarSubmenus()` (bottom of `navigation.blade.php`). Uses `data-sidebar-toggle` attribute, `e.stopPropagation()`, and `cloneNode()` to replace buttons and prevent duplicate listeners. Does **not** depend on Alpine.js (theme JS conflict prevented Alpine `x-on:click` from working).
---
## 6. Modular & Arsitektur
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `nwidart/laravel-modules` | ^13.0 | Memisahkan fitur ke folder `Modules/` agar codebase rapi |
### Custom Exception Hierarchy
`App\Exceptions\*` — domain-specific exceptions instead of generic `\Exception`:
- `SystemConfigException::unknownKey()`, `::imageUploadFailed()`
- `BackupOperationException::missingBinary()`, `::diskNotConfigured()`, `::restoreFailed()`
- `MonitoringException::unsupportedOs()`, `::probeFailed()`
---
## 7. Admin Panel & API Docs
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `filament/filament` | ^5.5 | Admin panel builder (resource management cepat) |
| `darkaonline/l5-swagger` | ^11.0 | Auto-generate Swagger/OpenAPI docs dari annotation. Spec di `storage/api-docs/`. |
---
## 8. Monitoring & Error Tracking
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `sentry/sentry-laravel` | ^4.25 | Error monitoring & performance tracking untuk production |
> Set `SENTRY_LARAVEL_DSN` di `.env` untuk mengaktifkan. Log error otomatis terkirim ke Sentry dashboard.
Endpoint `GET /api/health` mengembalikan status `database`/`redis`/`storage`/`queue`. Kembalikan `503` hanya saat ada check yang `fail``warn` (disk >90%) tetap `200`.
---
## 9. Frontend Build
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `vite` | ^7.0 | Build tool — hot reload & bundling JS/CSS |
| `laravel-vite-plugin` | ^2.0 | Integrasi Vite dengan Blade |
| `tailwindcss` | ^4.2 | CSS utility-first |
| `@tailwindcss/forms` | ^0.5.2 | Plugin Tailwind untuk styling form |
| `alpinejs` | ^3.4 | Reactive JS ringan (toggle, modal, tabs) |
| `axios` | ^1.15 | HTTP client untuk AJAX |
| `laravel-echo` | ^2.3 | Client untuk subscribe ke WebSocket channel |
| `pusher-js` | ^8.5 | Transport layer untuk Echo (kompatibel Reverb) |
| `rollup` | ^4.60 | Module bundler (digunakan Vite secara internal) |
| `concurrently` | ^9.0 | Menjalankan beberapa command paralel saat dev |
### Dev Script (`composer run dev`)
Menjalankan beberapa proses secara paralel:
| Proses | Command |
|--------|---------|
| SERVER | `php artisan serve --host=0.0.0.0 --port=8000` |
| VITE | `npm run dev` |
| QUEUE | `php artisan queue:listen --tries=1` |
### Scheduled Tasks (Production)
| Waktu | Command | Fungsi |
|-------|---------|--------|
| Setiap menit | `dashboard:broadcast-stats` | Broadcast stats dashboard ke WebSocket channel `admin.monitoring` (withoutOverlapping) |
| Setiap menit | `MaintenanceManagementService::autoCheckAndRelease()` | Auto-release maintenance mode |
| Setiap menit | `WorkerHeartbeatJob` | Queue worker monitoring |
| Setiap 30 menit | `system:health-check` | System health check |
| Harian 03:00 | `model:prune` | Pruning OtpCode, UserTrustedDevice, AiHealingLog, PasswordHistory, dll |
| Harian 03:05 | `telescope:prune --hours=48` | Hapus Telescope entries > 48 jam |
| Harian | `activitylog:clean` | Hapus activity log > 365 hari |
| Senin 07:00 | `backups:verify` | Verifikasi integritas backup |
| Senin 07:05 | `permissions:audit --json` | Audit permission (log only) |
| Senin 08:00 | `system:send-digest` | Weekly health digest ke admin |
| Dinamis | DB backup + cleanup | Frekuensi dikonfigurasi dari Global Settings |
> Untuk dev penuh (termasuk Reverb + Scheduler), pakai Sail (`./vendor/bin/sail up -d`).
---
## 10. Frontend Library (CDN/Blade)
Dimuat via CDN di template Blade:
| Library | Kegunaan |
|---------|----------|
| Bootstrap 5 | Layout grid & komponen UI |
| Bootstrap Icons | Ikon SVG |
| jQuery | DOM manipulation & AJAX |
| SweetAlert2 | Dialog & notifikasi toast |
| CKEditor 5 | WYSIWYG editor (Privacy Policy, ToS, About, dll) |
| FilePond | Upload file drag-and-drop |
| Animate.css | Animasi entrance/exit elemen |
| Marked.js | Render Markdown untuk laporan analisis AI |
| Choices.js | Dropdown searchable & multi-select |
| SortableJS | Drag-to-reorder dashboard widget grid (loaded via CDN in dashboard.blade.php) |
| Google Fonts | Inter, Outfit, Fira Code |
---
## 11. Development & Quality Tools
### Code Quality
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `laravel/pint` | ^1.24 | Code formatter (PSR-12). Wajib hijau sebelum merge. |
| `larastan/larastan` | ^3.9 | Static analysis Laravel-aware (PHPStan). Level 5 + baseline. |
| `laravel/sail` | ^1.41 | Docker dev environment (app + Postgres + Redis) |
| `laravel/pail` | ^1.2 | Live log viewer di terminal |
| `laravel/telescope` | ^5.20 | Debug tool (request, query, job, mail) — hanya dev |
| `laravel/boost` | ^2.0 | AI assistant untuk Laravel dev |
### Testing
| Package | Versi | Kegunaan |
|---------|-------|----------|
| `pestphp/pest` | ^4.0 | Testing framework modern |
| `pestphp/pest-plugin-laravel` | ^4.0 | Helper Pest untuk Laravel |
| `mockery/mockery` | ^1.6 | Library mocking untuk test |
| `fakerphp/faker` | ^1.23 | Generator data dummy |
| `nunomaduro/collision` | ^8.6 | Error reporting yang readable di terminal |
### Test Suite Statistics
| Kategori | File | Tests |
|----------|------|-------|
| Feature: Auth + WebAuthn + Social + 2FA + Impersonate | 9 | ~50 |
| Feature: AccessControl (User/Role/Permission) | 3 | 37 |
| Feature: Middleware (IP, ActivePermission, Legal, PwdExpiry, SecurityHeaders, CheckTabPermission) | 6 | 30 |
| Feature: Services (SystemConfig, PasswordPolicy, Backup) | 3 | 31 |
| Feature: Performance (N+1 regression) | 1 | 3 |
| Feature: Database (FK + Cascade) | 1 | 9 |
| Feature: API (Health, MobileConfig, Rate-limit, OTP, AuthAPI, DeviceToken) | 6 | 25 |
| Feature: Dashboard (widget prefs, broadcast event) | 2 | 18 |
| Feature: Helpers (ApiResponse, PasswordRule) | 2 | 18 |
| Unit: Pure logic (Formatter, Caster, Helpers, Exceptions) | 5 | 88 |
| Granular tab permission system | — | +62 |
| **Total** | **38** | **371** |
Run via `./vendor/bin/sail artisan test`. Avg runtime ~35s.
---
## 12. CI/CD
Workflow di `.github/workflows/ci.yml` (GitHub Actions). 3 job paralel:
| Job | Tools |
|-----|-------|
| `test` | Pest 4 (Postgres 15 + Redis 7 service containers) |
| `lint` | `pint --test` + `composer audit` + `permissions:audit` |
| `static-analysis` | Larastan level 5 + baseline |
Push ke `main`/`develop`/`config`/`advanced` dan PR ke `main`/`develop` mentrigger pipeline.
---
## 13. Integrasi Eksternal (Opsional)
Sebagian besar diatur dari **Global Settings** di admin panel — tidak perlu edit `.env`.
| Layanan | Kegunaan |
|---------|----------|
| **OpenAI GPT** | AI assistant di admin panel |
| **Google Gemini** | AI assistant alternatif |
| **Anthropic Claude** | AI assistant alternatif |
| **DeepSeek** | AI assistant alternatif |
| **xAI Grok** | AI assistant alternatif |
| **Mistral AI** | AI assistant alternatif |
| **OpenRouter** | Gateway multi-provider AI |
| **SAP NW RFC** | Koneksi ke sistem SAP ERP |
| **Google Drive** | Cloud backup |
| **Amazon S3** | Cloud backup |
| **SMTP (Mailgun/SES)** | Pengiriman email transaksional |
| **Telegram Bot** | Notifikasi ke channel Telegram (incl. firewall block alert) |
| **Google reCAPTCHA** | Anti-bot di form login |
| **Firebase Cloud Messaging** | Push notification ke mobile (device token) |
| **Sentry** | Error monitoring & performance tracing |
---
## Ringkasan Arsitektur
```
┌─────────────────────────────────────────────────────────┐
│ Browser / Mobile App (React Native) │
└────────────┬────────────────────────────────┬───────────┘
│ HTTPS (+ security headers) │ HTTPS + WS
▼ ▼
┌──────────┐ ┌──────────┐
│ Nginx │◄────────────────────│ Reverb │ WebSocket
└─────┬────┘ └─────┬────┘
▼ ▼
┌──────────────────────────────────────────────┐
│ Laravel 13 (PHP-FPM) │
│ │
│ Global middleware: │
│ ┌──────────────────────────────────────┐ │
│ │ SecurityHeaders │ │
│ │ IpAccessControl │ │
│ │ PasswordExpiry │ │
│ │ CheckLegalAgreement │ │
│ │ ThrottleRequests (per route) │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Web │ │ API v1 │ │ Reverb │ │
│ │ Routes │ │ Sanctum │ │ Broadcast │ │
│ └────┬─────┘ └────┬─────┘ └──────┬──────┘ │
└───────┼────────────┼──────────────┼──────────┘
│ │ │
┌──────────▼───┐ ┌────▼──┐ ┌──────▼─────┐
│ PostgreSQL 15 │ │Redis 7│ │ Filesystem │
│ (data utama) │ │cache, │ │ local/S3/ │
│ FK + indexes │ │queue, │ │ GDrive │
│ + cascade │ │session│ │ │
└───────────────┘ └───────┘ └────────────┘
┌──────▼──────┐
│ Sentry │
│ (error mon) │
└─────────────┘
```
+262
View File
@@ -0,0 +1,262 @@
# User Guide — Panduan Admin
Panduan ini untuk **Administrator** atau **Super Admin** yang mengoperasikan aplikasi.
---
## Login
1. Buka `https://domain.com/login`
2. Masukkan email & password
3. (Opsional) jika **2FA** diaktifkan, kode 6 digit dikirim ke email — input di halaman `/2fa`
4. (Opsional) centang **Trust this device** untuk skip 2FA selama 30 hari (cookie aman)
5. (Opsional) gunakan **Passkey (biometrik)** jika sudah didaftarkan di profil
6. (Opsional) login via **Google / Facebook / GitHub** jika Social OAuth diaktifkan
> Akun default setelah seeder: lihat tabel di README.md. Jika perlu reset, jalankan:
> ```bash
> ./vendor/bin/sail artisan db:seed --class=AdminUserSeeder
> ./vendor/bin/sail artisan cache:clear
> ```
### Rate Limit
Untuk mencegah brute force:
| Endpoint | Limit |
|----------|-------|
| `/login` (web) | 5/menit per IP |
| `/2fa` verify | 5/menit per IP |
| `/forgot-password` | dibatasi via Spatie throttle |
| `/api/v1/login` (mobile) | 10/menit per IP |
| `/api/v1/otp/send` | 5/menit per IP |
| `/api/v1/otp/verify` | 10/menit per IP |
Jika kena rate limit, response `429 Too Many Requests` — tunggu 1 menit.
---
## Dashboard
Halaman pertama setelah login. Menampilkan statistik sistem secara **real-time** via WebSocket (Laravel Reverb). Jika koneksi WebSocket tidak tersedia, data di-refresh otomatis tiap 30 detik.
### Widget Bawaan
| Widget | Isi |
|--------|-----|
| CPU Load | Persentase penggunaan CPU dengan sparkline |
| Memory | RAM used/total |
| Storage | Disk used/total |
| Live Users | Sesi aktif saat ini |
| Queue Stats | Job pending/processed/failed |
| Activity Feed | Log aktivitas terbaru (butuh permission `view health and logs`) |
| AI Security Insight | Skor keamanan terakhir dari AI analysis (butuh permission `view ai log analysis`) |
### Kustomisasi Dashboard
1. Klik tombol **Customize** di pojok kanan atas halaman Dashboard
2. Panel kustomisasi muncul dengan daftar widget:
- Toggle switch untuk **tampilkan/sembunyikan** setiap widget
- Drag-and-drop widget untuk **mengubah urutan** tampilan
3. Klik **Save Layout** — preferensi tersimpan di database per akun
4. Klik **Reset to Default** untuk kembali ke urutan dan visibilitas bawaan
> Preferensi layout **per-user** — setiap admin bisa punya tata letak sendiri.
---
## Menu Utama
### 1. Users — Manajemen Pengguna
**Akses:** sidebar → User Management
- Tambah / edit / hapus user (soft delete — bisa di-restore)
- Atur role (Super Admin, Admin, Custom)
- Aktif / nonaktifkan akun
- **Bulk action** — aktifkan/nonaktifkan/hapus banyak user sekaligus
- **Impersonate** — login sebagai user lain untuk debugging
- Tidak bisa impersonate diri sendiri
- Tidak bisa impersonate Super Admin/Developer
- Tidak bisa impersonate user inactive
- Tidak bisa nested impersonate (loop prevention)
> Force delete diri sendiri di-blokir oleh sistem — gunakan akun admin lain bila perlu.
### 2. Roles & Permissions
**Akses:** sidebar → Access Control
- Buat role baru dan pilih permission yang diizinkan
- **Permission dikelompokkan per kategori** dalam dua panel:
- Panel kiri (**Available**) — permission yang belum diberikan ke role ini
- Panel kanan (**Assigned**) — permission yang sudah diberikan
- Pindahkan dengan **drag-and-drop** atau **double-click** item
- Gunakan **search** di masing-masing panel untuk filter cepat
- **Category group headers** muncul otomatis di panel Assigned saat ada permission dari kategori tersebut
- **85 granular tab permissions** — setiap tab di Global Settings dan Mobile Settings punya permission sendiri (mis. `manage global settings password policy`, `view mobile settings kill switch`)
- Role dan permission bisa diaktif/nonaktifkan tanpa dihapus
- Audit trail tersedia: setiap perubahan role/permission tercatat
- Tidak bisa archive role yang masih dipakai user — pindahkan user dulu
### 3. Action Logs — Audit Trail
**Akses:** sidebar → Action History
Mencatat semua perubahan data: siapa, kapan, apa yang diubah, dari IP mana. Berguna untuk audit & forensik.
Sensitive fields (password, token, secret, 2fa_secret) otomatis di-redact dari log entry.
---
## System Settings
### Global Settings
**Akses:** System Settings → Global Settings
| Tab | Pengaturan |
|-----|------------|
| General (Branding) | Nama app, logo, favicon, tagline, footer, landing page visibility, locale |
| Login Security | Max attempts, lockout duration, 2FA, OTP, captcha (reCAPTCHA v2/v3), passkey (WebAuthn) |
| Password Policy | Panjang min/max, karakter wajib (uppercase/lowercase/angka/simbol), expiry, riwayat |
| Social Login / OAuth | Toggle Google/Facebook/GitHub OAuth, client ID & secret, callback URL |
| IP & Access | Whitelist/blacklist IP admin, CORS, rate limit, force HTTPS, HSTS |
| Notification | SMTP (host, port, encryption, from address), Telegram bot token & chat ID |
| AI Config | Provider (GPT/Gemini/Claude/DeepSeek/Grok/Mistral/OpenRouter), API key, model, temperature |
| SAP Integration | RFC host, system number, client, user, password — termasuk tombol Test Connection |
| Backup & Storage | Driver (local/S3/GDrive), jadwal, retensi, enkripsi, notifikasi |
| Maintenance | Toggle, pesan, judul, countdown, secret key, IP whitelist |
| Legal & Content | Privacy Policy, Terms of Use, About, Security Policy, Help Center (editor WYSIWYG) |
| Regional | Timezone, format tanggal, format waktu |
| Session | Driver, lifetime, single session, auto logout, remember me, cookie settings |
> Tombol **Save Configuration** selalu muncul di pojok kanan bawah (floating). Setelah save, cache otomatis di-invalidate.
### Mobile Settings
**Akses:** System Settings → Mobile Settings
Kontrol konfigurasi aplikasi mobile dari jarak jauh — warna tema, base URL API, FCM topic, biometric login, kill switch, pesan maintenance mobile, dll. Perubahan langsung tersinkron ke aplikasi mobile via endpoint `/api/v1/mobile/sync` (dengan ETag caching).
### Maintenance Mode
**Akses:** System Settings → Maintenance (atau via tab di Global Settings)
1. Aktifkan toggle **Enable Maintenance Mode**
2. Isi judul & pesan untuk pengunjung
3. (Opsional) set **End Time** untuk countdown timer — aplikasi auto-release saat waktu berlalu
4. (Opsional) isi **Secret Key** — admin bisa tetap akses via `domain.com/{secret}`
5. (Opsional) isi **IP Whitelist** — IP yang dikecualikan dari maintenance
6. Klik **Apply Maintenance Settings**
> Live preview ditampilkan persis seperti yang dilihat pengunjung.
### Backup & Restore
**Akses:** System Settings → Backup & Storage
- Pilih **driver**: Local / Amazon S3 / Google Drive
- Atur jadwal backup otomatis, retensi (berapa backup disimpan), dan enkripsi AES-256
- Operasi: Run Backup Now, Download, Restore, Delete
- Notifikasi backup bisa dikirim ke email atau webhook
- Tombol **Test Connection** untuk verifikasi driver sebelum simpan
### System Monitoring
**Akses:** System Settings → System Monitoring
| Tab | Isi |
|-----|-----|
| Logs | Laravel error log (filter level, search, download, clear) |
| SAP Logs | Log integrasi SAP RFC — status request dan error |
| Mobile Logs | Log yang dikirim dari aplikasi mobile (`/api/v1/mobile/log`) |
| Background Jobs | Status queue, retry / delete failed jobs |
| AI Usage | Riwayat penggunaan AI — provider, model, token, waktu |
| Health | CPU, memory, disk usage secara real-time |
### Notification Center
**Akses:** sidebar → Notifications (ikon lonceng)
Pusat notifikasi sistem real-time via WebSocket (Reverb). Admin bisa:
- Melihat notifikasi masuk
- Menandai sudah dibaca (satu atau semua)
- Broadcast notifikasi ke semua user atau role tertentu
### Session Manager
**Akses:** System Settings → Session Manager
Lihat semua sesi aktif seluruh user. Bisa memaksa logout user tertentu jika dicurigai kompromi akun.
Jika `Single Session` aktif di Global Settings, user otomatis di-logout dari device lama saat login di device baru.
---
## Profil & Keamanan Akun
**Akses:** klik nama/avatar di pojok kanan atas → Profile
- Update nama, email, avatar
- Ganti password (cek policy: min 12, mixed-case, digit, symbol; tidak boleh sama dengan password sebelumnya jika history blocker aktif)
- Daftarkan / hapus **Passkey (biometrik)**
- Lihat & cabut sesi aktif
### Password Expiry
Jika admin mengaktifkan **Password Expiry** di Global Settings → Password Policy, user akan diarahkan ke `/profile/password` saat password lewat masa berlaku, dengan pesan warning.
---
## Halaman Legal (UU PDP)
Halaman publik yang bisa diakses tanpa login:
| URL | Konten |
|-----|--------|
| `/legal/privacy` | Kebijakan Privasi |
| `/legal/tos` | Syarat & Ketentuan |
| `/legal/about` | Tentang Biiproject |
| `/legal/security` | Kebijakan Keamanan |
| `/legal/help` | Pusat Bantuan & FAQ |
Konten semua halaman ini dikelola via Global Settings → Legal & Content.
Jika versi dokumen (`pdp_document_version` atau `tos_document_version`) diperbarui, user yang sudah login akan diarahkan ke halaman **re-agree** (`/legal/re-agree`) dan wajib menyetujui ulang sebelum bisa mengakses aplikasi.
---
## Tips Operasional
1. **Setelah menjalankan seeder**, selalu clear cache: `./vendor/bin/sail artisan cache:clear`
2. **Backup sebelum update** besar atau perubahan konfigurasi penting
3. Selalu uji koneksi (Email, SAP, Backup) lewat tombol **Test Connection** sebelum save
4. Setelah ubah `.env`, jalankan: `./vendor/bin/sail artisan optimize:clear`
5. Cek **Action Logs** secara berkala untuk audit keamanan
6. **Maintenance Mode**: selalu set Secret Key agar tidak terkunci dari panel sendiri
7. **IP Whitelist Admin**: tambahkan dulu IP statis Anda sebelum mengaktifkan, agar tidak terkunci
8. **2FA**: untuk akun super admin, sangat disarankan diaktifkan
9. **Single Session**: untuk environment sensitif (PII, finance), aktifkan agar 1 akun = 1 device
---
## Bantuan & Troubleshooting
| Masalah | Solusi |
|---------|--------|
| Lupa password admin | `./vendor/bin/sail artisan tinker` → reset via model `User` |
| Panel tidak responsif / data lama | `./vendor/bin/sail artisan optimize:clear` + `cache:clear` |
| Error 500 | Cek `storage/logs/laravel.log` atau menu System Monitoring → Logs |
| Setting yang baru disimpan tidak muncul | `./vendor/bin/sail artisan cache:clear` |
| User tidak bisa login | Cek status akun di User Management, pastikan role & permission aktif |
| Notifikasi real-time tidak muncul | Pastikan Reverb berjalan: `supervisorctl status biiproject-reverb` |
| Dashboard stats tidak update otomatis | Cek koneksi WebSocket di browser DevTools (Console). Jika gagal, refresh — akan fallback ke polling 30 detik. |
| Widget tersimpan tidak muncul | Buka Customize panel, klik Reset to Default, lalu coba Save Layout ulang. |
| Tab Settings tidak bisa diakses | Permission tab-level mungkin belum diberikan ke role Anda — minta Super Admin assign via Roles & Permissions. |
| Kena rate limit terus | Cek IP whitelist admin, tunggu 1 menit, atau reset via `php artisan cache:clear` |
| 2FA email tidak masuk | Verifikasi SMTP di Global Settings → Notifications + cek log queue |
| OAuth Google/Facebook gagal | Cek `/auth/callback` reachable (bukan 404), Client ID/Secret valid, redirect URI cocok |
| Health endpoint return 503 | Buka `/api/health`, lihat field `checks.*` — yang `fail` mengindikasikan dependensi rusak. `warn` masih 200. |
| User di-logout otomatis | Single-session mungkin aktif (login dari device lain), atau password expired |
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# --- Colors for Output ---
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${BLUE}==============================================${NC}"
echo -e "${GREEN} Production Readiness Checklist ${NC}"
echo -e "${BLUE}==============================================${NC}"
# 1. Check PHP Version
PHP_VER=$(php -r 'echo PHP_VERSION;')
echo -ne "Checking PHP Version (8.2+ required)... "
if [[ $(echo "$PHP_VER 8.2" | awk '{print ($1 >= $2)}') -eq 1 ]]; then
echo -e "${GREEN}OK ($PHP_VER)${NC}"
else
echo -e "${RED}FAIL ($PHP_VER)${NC}"
fi
# 2. Check PHP Extensions
echo -e "\nChecking PHP Extensions:"
EXTENSIONS=("pgsql" "redis" "curl" "mbstring" "xml" "zip" "bcmath" "intl" "gd")
for ext in "${EXTENSIONS[@]}"; do
echo -ne " - $ext... "
if php -m | grep -qi "$ext"; then
echo -e "${GREEN}INSTALLED${NC}"
else
echo -e "${RED}MISSING${NC}"
fi
done
# 3. Check .env settings
echo -e "\nChecking .env settings:"
if [ -f .env ]; then
APP_ENV=$(grep APP_ENV .env | cut -d '=' -f2)
APP_DEBUG=$(grep APP_DEBUG .env | cut -d '=' -f2)
echo -ne " - APP_ENV... "
if [[ "$APP_ENV" == "production" ]]; then
echo -e "${GREEN}production${NC}"
else
echo -e "${YELLOW}$APP_ENV (Warning: not production)${NC}"
fi
echo -ne " - APP_DEBUG... "
if [[ "$APP_DEBUG" == "false" ]]; then
echo -e "${GREEN}false${NC}"
else
echo -e "${RED}true (CRITICAL: Disable debug in production!)${NC}"
fi
else
echo -e "${RED}.env file missing!${NC}"
fi
# 4. Check Directory Permissions
echo -e "\nChecking Directory Permissions:"
DIRS=("storage" "bootstrap/cache")
for dir in "${DIRS[@]}"; do
echo -ne " - $dir writable... "
if [ -w "$dir" ]; then
echo -e "${GREEN}YES${NC}"
else
echo -e "${RED}NO${NC}"
fi
done
# 5. Check Mobile API URL
echo -e "\nChecking Mobile API Configuration:"
if [ -f mobile/services/api.ts ]; then
if grep -q "Constants.expoConfig?.extra?.apiUrl" mobile/services/api.ts; then
echo -e "${GREEN}Flexible (Production Ready)${NC}"
else
echo -e "${YELLOW}Hardcoded (Check mobile/services/api.ts)${NC}"
fi
fi
# 6. Check Database Connectivity
echo -e "\nChecking Database Connectivity... "
php artisan db:monitor --quiet > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo -e "${GREEN}CONNECTED${NC}"
else
echo -e "${RED}FAILED${NC}"
fi
echo -e "\n${BLUE}==============================================${NC}"
echo -e "${YELLOW}Next Steps for Smooth Deployment:${NC}"
echo -e "1. Run ${BLUE}php artisan optimize${NC} in production."
echo -e "2. Run ${BLUE}npm run build${NC} for frontend assets."
echo -e "3. Ensure ${BLUE}Supervisor${NC} is running for Queue and Reverb."
echo -e "4. Check ${BLUE}Nginx${NC} logs for any proxy issues."
echo -e "${BLUE}==============================================${NC}"
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* SAP RFC Environment Checker
* run with: php check-sap.php
*/
echo "\e[1;34m=== SAP RFC ENVIRONMENT CHECKER ===\e[0m\n\n";
// 1. Check PHP Extension
echo "[1] Checking PHP Extension 'sapnwrfc'...\n";
if (extension_loaded('sapnwrfc')) {
echo "\e[32m[OK]\e[0m Extension 'sapnwrfc' is loaded.\n";
} else {
echo "\e[31m[FAIL]\e[0m Extension 'sapnwrfc' is NOT loaded.\n";
echo " Tip: Check your php.ini and ensure 'extension=sapnwrfc.so' is present.\n";
}
// 2. Check SAPNWRFC Class
echo "\n[2] Checking SAPNWRFC Class existence...\n";
if (class_exists('SAPNWRFC\Connection')) {
echo "\e[32m[OK]\e[0m SAPNWRFC classes are available.\n";
} else {
echo "\e[31m[FAIL]\e[0m SAPNWRFC classes are NOT found.\n";
}
// 3. Check SAP SDK via ldconfig
echo "\n[3] Checking SAP NW RFC SDK in system libraries...\n";
$ldOutput = shell_exec('ldconfig -p | grep sap');
if ($ldOutput) {
echo "\e[32m[OK]\e[0m SAP libraries found in ldconfig:\n";
echo $ldOutput;
} else {
echo "\e[33m[WARNING]\e[0m No SAP libraries found in ldconfig.\n";
echo " You might need to add the SDK 'lib' path to /etc/ld.so.conf.d/sap.conf\n";
}
// 4. Check for common SDK locations
echo "\n[4] Searching for SDK in common paths...\n";
$commonPaths = [
'/usr/local/sap/nwrfcsdk',
'/opt/sap/nwrfcsdk',
'/usr/sap/nwrfcsdk',
];
$found = false;
foreach ($commonPaths as $path) {
if (is_dir($path)) {
echo "\e[32m[FOUND]\e[0m SDK directory detected at: $path\n";
$found = true;
}
}
if (! $found) {
echo "\e[31m[NOT FOUND]\e[0m Could not find SDK in common locations.\n";
}
// 5. Check Project Config
echo "\n[5] Checking Project .env configuration...\n";
$envContent = file_get_contents('.env');
if (strpos($envContent, 'SAP_RFC_ASHOST') !== false) {
echo "\e[32m[OK]\e[0m SAP configuration keys found in .env\n";
} else {
echo "\e[33m[INFO]\e[0m SAP keys not found in .env. Ensure you've saved them via the System Configuration page.\n";
}
echo "\n\e[1;34m=== CHECK COMPLETED ===\e[0m\n";
+331
View File
@@ -0,0 +1,331 @@
parameters:
ignoreErrors:
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$name\.$#'
identifier: property.notFound
count: 1
path: app/Events/ActivityLogCreated.php
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$email\.$#'
identifier: property.notFound
count: 2
path: app/Http/Controllers/AccessControl/ActionLogController.php
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$name\.$#'
identifier: property.notFound
count: 3
path: app/Http/Controllers/AccessControl/ActionLogController.php
-
message: '#^Call to function is_array\(\) with Illuminate\\Support\\Collection\|null will always evaluate to false\.$#'
identifier: function.impossibleType
count: 1
path: app/Http/Controllers/AccessControl/ActionLogController.php
-
message: '#^Using nullsafe property access "\?\-\>email" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 2
path: app/Http/Controllers/AccessControl/ActionLogController.php
-
message: '#^Using nullsafe property access "\?\-\>name" on left side of \?\? is unnecessary\. Use \-\> instead\.$#'
identifier: nullsafe.neverNull
count: 3
path: app/Http/Controllers/AccessControl/ActionLogController.php
-
message: '#^Access to an undefined property App\\Models\\Permission\:\:\$is_active\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/AccessControl/PermissionManagementController.php
-
message: '#^Relation ''creator'' is not found in App\\Models\\Permission model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/PermissionManagementController.php
-
message: '#^Relation ''updater'' is not found in App\\Models\\Permission model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/PermissionManagementController.php
-
message: '#^Access to an undefined property App\\Models\\Role\:\:\$is_active\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/AccessControl/RoleManagementController.php
-
message: '#^Relation ''creator'' is not found in App\\Models\\Role model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/RoleManagementController.php
-
message: '#^Relation ''updater'' is not found in App\\Models\\Role model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/RoleManagementController.php
-
message: '#^Relation ''creator'' is not found in App\\Models\\User model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/UserManagementController.php
-
message: '#^Relation ''updater'' is not found in App\\Models\\User model\.$#'
identifier: larastan.relationExistence
count: 3
path: app/Http/Controllers/AccessControl/UserManagementController.php
-
message: '#^Called ''pluck'' on Laravel collection, but could have been retrieved as a query\.$#'
identifier: larastan.noUnnecessaryCollectionCall
count: 1
path: app/Http/Controllers/Admin/MobileSettingController.php
-
message: '#^Call to an undefined method Illuminate\\Cookie\\CookieJar\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Http/Controllers/Auth/AuthenticatedSessionController.php
-
message: '#^Access to an undefined property Laravel\\Socialite\\Contracts\\User\:\:\$avatar\.$#'
identifier: property.notFound
count: 2
path: app/Http/Controllers/Auth/SocialAuthController.php
-
message: '#^Access to an undefined property Laravel\\Socialite\\Contracts\\User\:\:\$email\.$#'
identifier: property.notFound
count: 3
path: app/Http/Controllers/Auth/SocialAuthController.php
-
message: '#^Access to an undefined property Laravel\\Socialite\\Contracts\\User\:\:\$id\.$#'
identifier: property.notFound
count: 3
path: app/Http/Controllers/Auth/SocialAuthController.php
-
message: '#^Parameter \#1 \$user of class Illuminate\\Auth\\Events\\Verified constructor expects Illuminate\\Contracts\\Auth\\MustVerifyEmail, App\\Models\\User\|null given\.$#'
identifier: argument.type
count: 1
path: app/Http/Controllers/Auth/VerifyEmailController.php
-
message: '#^Strict comparison using \=\=\= between ''about''\|''help''\|''security''\|''tos'' and ''pdp'' will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: app/Http/Controllers/LegalController.php
-
message: '#^Access to an undefined property Laravel\\Socialite\\Contracts\\User\:\:\$refreshToken\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/SystemSettings/BackupRestoreController.php
-
message: '#^Call to an undefined method Laravel\\Socialite\\Contracts\\Provider\:\:scopes\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Http/Controllers/SystemSettings/BackupRestoreController.php
-
message: '#^Access to an undefined property App\\Models\\Notification\:\:\$personal_read_at\.$#'
identifier: property.notFound
count: 1
path: app/Http/Controllers/SystemSettings/NotificationCenterController.php
-
message: '#^Parameter \#1 \$notification of class App\\Events\\NotificationSent constructor expects App\\Models\\Notification, Illuminate\\Database\\Eloquent\\Model given\.$#'
identifier: argument.type
count: 1
path: app/Http/Controllers/SystemSettings/NotificationCenterController.php
-
message: '#^Call to method close\(\) on an unknown class SAPNWRFC\\Connection\.$#'
identifier: class.notFound
count: 1
path: app/Http/Controllers/SystemSettings/SystemConfigController.php
-
message: '#^Call to method getAttributes\(\) on an unknown class SAPNWRFC\\Connection\.$#'
identifier: class.notFound
count: 1
path: app/Http/Controllers/SystemSettings/SystemConfigController.php
-
message: '#^Instantiated class SAPNWRFC\\Connection not found\.$#'
identifier: class.notFound
count: 1
path: app/Http/Controllers/SystemSettings/SystemConfigController.php
-
message: '#^Method App\\Http\\Controllers\\WebAuthn\\WebAuthnRegisterController\:\:register\(\) should return Illuminate\\Http\\Response but returns Illuminate\\Http\\JsonResponse\.$#'
identifier: return.type
count: 1
path: app/Http/Controllers/WebAuthn/WebAuthnRegisterController.php
-
message: '#^Variable \$updates in empty\(\) always exists and is not falsy\.$#'
identifier: empty.variable
count: 1
path: app/Http/Requests/SystemSettings/UpdateSystemConfigRequest.php
-
message: '#^Parameter \#1 \$modelOrId of method Spatie\\Activitylog\\ActivityLogger\:\:causedBy\(\) expects Illuminate\\Database\\Eloquent\\Model\|int\|string\|null, Illuminate\\Contracts\\Auth\\Authenticatable\|null given\.$#'
identifier: argument.type
count: 1
path: app/Listeners/LogFailedLogin.php
-
message: '#^Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable\:\:\$id\.$#'
identifier: property.notFound
count: 1
path: app/Listeners/LogLogout.php
-
message: '#^Negated boolean expression is always false\.$#'
identifier: booleanNot.alwaysFalse
count: 1
path: app/Listeners/LogLogout.php
-
message: '#^Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable\:\:\$id\.$#'
identifier: property.notFound
count: 1
path: app/Listeners/LogSuccessfulLogin.php
-
message: '#^Access to an undefined property App\\Models\\Permission\:\:\$created_by\.$#'
identifier: property.notFound
count: 1
path: app/Observers/PermissionObserver.php
-
message: '#^Access to an undefined property App\\Models\\Permission\:\:\$updated_by\.$#'
identifier: property.notFound
count: 2
path: app/Observers/PermissionObserver.php
-
message: '#^Access to an undefined property App\\Models\\Role\:\:\$created_by\.$#'
identifier: property.notFound
count: 1
path: app/Observers/RoleObserver.php
-
message: '#^Access to an undefined property App\\Models\\Role\:\:\$updated_by\.$#'
identifier: property.notFound
count: 3
path: app/Observers/RoleObserver.php
-
message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:role\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Repositories/UserRepository.php
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$name\.$#'
identifier: property.notFound
count: 1
path: app/Services/AI/LogAnalysisService.php
-
message: '#^Parameter \#1 \$callback of method Illuminate\\Database\\Eloquent\\Collection\<int,Spatie\\Activitylog\\Models\\Activity\>\:\:map\(\) contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: app/Services/AI/LogAnalysisService.php
-
message: '#^Right side of && is always true\.$#'
identifier: booleanAnd.rightAlwaysTrue
count: 1
path: app/Services/MobileConfig/MobileConfigService.php
-
message: '#^Call to an undefined method Illuminate\\Redis\\Connections\\Connection\:\:executeRaw\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Services/Monitoring/SystemMonitoringService.php
-
message: '#^Parameter \#1 \$value of function count expects array\|Countable, string given\.$#'
identifier: argument.type
count: 1
path: app/Services/Monitoring/SystemMonitoringService.php
-
message: '#^Parameter \#2 \$pattern of method Redis\:\:scan\(\) expects string\|null, array\<string, int\|string\> given\.$#'
identifier: argument.type
count: 1
path: app/Services/Monitoring/SystemMonitoringService.php
-
message: '#^Parameter \#3 \$callback of static method Illuminate\\Cache\\Repository\:\:remember\(\) contains unresolvable type\.$#'
identifier: argument.unresolvableType
count: 1
path: app/Services/Monitoring/SystemMonitoringService.php
-
message: '#^Offset ''description'' on array\{type\: ''bool''\|''float''\|''image_path''\|''int''\|''string''\|''text'', group\: ''ai_config''\|''backups''\|''branding''\|''content_legal''\|''feature_flags''\|''ip_access''\|''login_security''\|''maintenance''\|''monitoring''\|''notifications''\|''password_policy''\|''regional''\|''sap_integration''\|''session_security'', is_public\: bool, default\: 0\|0\.7\|1\|5\|7\|8\|15\|30\|60\|64\|100\|120\|587\|2000\|3600\|''''\|''\*''\|''/auth/callback''\|''0''\|''00''\|''02\:00''\|''100''\|''Asia/Jakarta''\|''d/m/Y''\|''daily''\|''email''\|''en''\|''failed''\|''gpt''\|''gpt\-4o''\|''H\:i''\|''http\://localhost…''\|''Laravel''\|''LaravelBackups''\|''local''\|''noreply@example\.com''\|''Production''\|''redis''\|''Scheduled System…''\|''smtp''\|''System Notification''\|''The system is…''\|''tls''\|''us\-east\-1''\|''v2''\|bool\|null, description\: ''2FA Method''\|''About Us Content''\|''Active AI provider''\|''Admin IP Whitelist …''\|''AES\-256 Encryption''\|''AI max tokens''\|''AI temperature''\|''Allow Remember Me…''\|''Allowed CORS Headers''\|''Allowed CORS Methods''\|''Allowed CORS Origins''\|''Anthropic Claude…''\|''Application favicon…''\|''Application logo…''\|''Application name''\|''Auto Logout Idle …''\|''Automatically block…''\|''Backup Frequency''\|''Bypass secret key''\|''Compress with gzip''\|''Concurrent Session…''\|''Contact email for…''\|''Current version of…''\|''Date display format''\|''DeepSeek API Key''\|''Default AI model''\|''Default language''\|''Default system…''\|''Default Telegram…''\|''Description text''\|''Email Driver \(smtp,…''\|''Enable 2FA''\|''Enable AI services''\|''Enable API…''\|''Enable automated…''\|''Enable Captcha''\|''Enable Cookie…''\|''Enable HTTP Strict…''\|''Enable Laravel…''\|''Enable maintenance…''\|''Enable Passkeys …''\|''Enable/Disable the…''\|''Encrypt Session Data''\|''Encryption \(tls/ssl…''\|''Encryption Key \(min…''\|''Environment…''\|''Estimated end time''\|''Excluded Tables …''\|''Execution Time''\|''Facebook App ID''\|''Facebook App Secret''\|''Footer text''\|''Force HTTPS…''\|''GitHub Client ID''\|''GitHub Client Secret''\|''Global IP Blacklist…''\|''Global Social Login…''\|''Google Client ID''\|''Google Client Secret''\|''Google Drive Client…''\|''Google Drive Folder…''\|''Google Drive…''\|''Google Gemini API…''\|''Help Center / FAQ…''\|''Hits threshold…''\|''Lockout Duration …''\|''Log Login Activity''\|''Maintenance…''\|''Maintenance message''\|''Maintenance page…''\|''Make PDP agreement…''\|''Max requests per…''\|''Maximum Login…''\|''Maximum Password…''\|''Minimum Password…''\|''Mistral AI API Key''\|''Notification Target…''\|''Notification…''\|''Notify upon Lockout''\|''Official company…''\|''Ollama Base URL''\|''Only allow 1 active…''\|''OpenAI GPT API Key''\|''OpenRouter API Key''\|''Password Reset Link…''\|''Password Validity …''\|''Prevent Password…''\|''Primary tagline''\|''Privacy Policy \(UU…''\|''reCAPTCHA Secret''\|''reCAPTCHA Site Key''\|''reCAPTCHA Version''\|''Remember Me…''\|''Remember Trusted…''\|''Require Lowercase…''\|''Require Numbers''\|''Require Symbols /…''\|''Require Uppercase…''\|''Retention Policy …''\|''Retry\-After seconds''\|''S3 Access Key''\|''S3 Bucket Name''\|''S3 Custom Endpoint …''\|''S3 Region''\|''S3 Secret Key''\|''SAP Application…''\|''SAP Client Number''\|''SAP Password''\|''SAP RFC Trace Level''\|''SAP Router string …''\|''SAP System Number''\|''SAP Username''\|''Secondary tagline''\|''Secure Cookie …''\|''Security Policy…''\|''Sender Display Name''\|''Sender Email Address''\|''Session Driver …''\|''Session Lifetime /…''\|''SMTP Host Server''\|''SMTP Password''\|''SMTP Port \(587/465…''\|''SMTP Username''\|''Storage Driver …''\|''System default…''\|''Telegram Bot Token''\|''Terms of Use Content''\|''Time display format…''\|''Toggle Facebook…''\|''Toggle GitHub OAuth…''\|''Toggle Google OAuth…''\|''Toggle notification…''\|''Whitelisted IP…''\|''xAI Grok API Key''\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/Services/System/GlobalSearchService.php
-
message: '#^Offset ''group'' on array\{type\: ''bool''\|''float''\|''image_path''\|''int''\|''string''\|''text'', group\: ''ai_config''\|''backups''\|''branding''\|''content_legal''\|''feature_flags''\|''ip_access''\|''login_security''\|''maintenance''\|''monitoring''\|''notifications''\|''password_policy''\|''regional''\|''sap_integration''\|''session_security'', is_public\: bool, default\: 0\|0\.7\|1\|5\|7\|8\|15\|30\|60\|64\|100\|120\|587\|2000\|3600\|''''\|''\*''\|''/auth/callback''\|''0''\|''00''\|''02\:00''\|''100''\|''Asia/Jakarta''\|''d/m/Y''\|''daily''\|''email''\|''en''\|''failed''\|''gpt''\|''gpt\-4o''\|''H\:i''\|''http\://localhost…''\|''Laravel''\|''LaravelBackups''\|''local''\|''noreply@example\.com''\|''Production''\|''redis''\|''Scheduled System…''\|''smtp''\|''System Notification''\|''The system is…''\|''tls''\|''us\-east\-1''\|''v2''\|bool\|null, description\: ''2FA Method''\|''About Us Content''\|''Active AI provider''\|''Admin IP Whitelist …''\|''AES\-256 Encryption''\|''AI max tokens''\|''AI temperature''\|''Allow Remember Me…''\|''Allowed CORS Headers''\|''Allowed CORS Methods''\|''Allowed CORS Origins''\|''Anthropic Claude…''\|''Application favicon…''\|''Application logo…''\|''Application name''\|''Auto Logout Idle …''\|''Automatically block…''\|''Backup Frequency''\|''Bypass secret key''\|''Compress with gzip''\|''Concurrent Session…''\|''Contact email for…''\|''Current version of…''\|''Date display format''\|''DeepSeek API Key''\|''Default AI model''\|''Default language''\|''Default system…''\|''Default Telegram…''\|''Description text''\|''Email Driver \(smtp,…''\|''Enable 2FA''\|''Enable AI services''\|''Enable API…''\|''Enable automated…''\|''Enable Captcha''\|''Enable Cookie…''\|''Enable HTTP Strict…''\|''Enable Laravel…''\|''Enable maintenance…''\|''Enable Passkeys …''\|''Enable/Disable the…''\|''Encrypt Session Data''\|''Encryption \(tls/ssl…''\|''Encryption Key \(min…''\|''Environment…''\|''Estimated end time''\|''Excluded Tables …''\|''Execution Time''\|''Facebook App ID''\|''Facebook App Secret''\|''Footer text''\|''Force HTTPS…''\|''GitHub Client ID''\|''GitHub Client Secret''\|''Global IP Blacklist…''\|''Global Social Login…''\|''Google Client ID''\|''Google Client Secret''\|''Google Drive Client…''\|''Google Drive Folder…''\|''Google Drive…''\|''Google Gemini API…''\|''Help Center / FAQ…''\|''Hits threshold…''\|''Lockout Duration …''\|''Log Login Activity''\|''Maintenance…''\|''Maintenance message''\|''Maintenance page…''\|''Make PDP agreement…''\|''Max requests per…''\|''Maximum Login…''\|''Maximum Password…''\|''Minimum Password…''\|''Mistral AI API Key''\|''Notification Target…''\|''Notification…''\|''Notify upon Lockout''\|''Official company…''\|''Ollama Base URL''\|''Only allow 1 active…''\|''OpenAI GPT API Key''\|''OpenRouter API Key''\|''Password Reset Link…''\|''Password Validity …''\|''Prevent Password…''\|''Primary tagline''\|''Privacy Policy \(UU…''\|''reCAPTCHA Secret''\|''reCAPTCHA Site Key''\|''reCAPTCHA Version''\|''Remember Me…''\|''Remember Trusted…''\|''Require Lowercase…''\|''Require Numbers''\|''Require Symbols /…''\|''Require Uppercase…''\|''Retention Policy …''\|''Retry\-After seconds''\|''S3 Access Key''\|''S3 Bucket Name''\|''S3 Custom Endpoint …''\|''S3 Region''\|''S3 Secret Key''\|''SAP Application…''\|''SAP Client Number''\|''SAP Password''\|''SAP RFC Trace Level''\|''SAP Router string …''\|''SAP System Number''\|''SAP Username''\|''Secondary tagline''\|''Secure Cookie …''\|''Security Policy…''\|''Sender Display Name''\|''Sender Email Address''\|''Session Driver …''\|''Session Lifetime /…''\|''SMTP Host Server''\|''SMTP Password''\|''SMTP Port \(587/465…''\|''SMTP Username''\|''Storage Driver …''\|''System default…''\|''Telegram Bot Token''\|''Terms of Use Content''\|''Time display format…''\|''Toggle Facebook…''\|''Toggle GitHub OAuth…''\|''Toggle Google OAuth…''\|''Toggle notification…''\|''Whitelisted IP…''\|''xAI Grok API Key''\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/Services/System/GlobalSearchService.php
-
message: '#^Unknown parameter \$user_id in call to App\\Events\\SystemNotification constructor\.$#'
identifier: argument.unknown
count: 1
path: app/Services/System/MaintenanceManagementService.php
-
message: '#^Call to an undefined method Symfony\\Component\\HttpFoundation\\File\\UploadedFile\:\:store\(\)\.$#'
identifier: method.notFound
count: 1
path: app/Services/SystemConfig/SettingFileUploader.php
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$type\.$#'
identifier: property.notFound
count: 1
path: app/Services/SystemConfig/SystemConfigService.php
-
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$value\.$#'
identifier: property.notFound
count: 1
path: app/Services/SystemConfig/SystemConfigService.php
-
message: '#^Offset ''description'' on array\{type\: ''bool''\|''float''\|''image_path''\|''int''\|''string''\|''text'', group\: ''ai_config''\|''backups''\|''branding''\|''content_legal''\|''feature_flags''\|''ip_access''\|''login_security''\|''maintenance''\|''monitoring''\|''notifications''\|''password_policy''\|''regional''\|''sap_integration''\|''session_security'', is_public\: bool, default\: 0\|0\.7\|1\|5\|7\|8\|15\|30\|60\|64\|100\|120\|587\|2000\|3600\|''''\|''\*''\|''/auth/callback''\|''0''\|''00''\|''02\:00''\|''100''\|''Asia/Jakarta''\|''d/m/Y''\|''daily''\|''email''\|''en''\|''failed''\|''gpt''\|''gpt\-4o''\|''H\:i''\|''http\://localhost…''\|''Laravel''\|''LaravelBackups''\|''local''\|''noreply@example\.com''\|''Production''\|''redis''\|''Scheduled System…''\|''smtp''\|''System Notification''\|''The system is…''\|''tls''\|''us\-east\-1''\|''v2''\|bool\|null, description\: ''2FA Method''\|''About Us Content''\|''Active AI provider''\|''Admin IP Whitelist …''\|''AES\-256 Encryption''\|''AI max tokens''\|''AI temperature''\|''Allow Remember Me…''\|''Allowed CORS Headers''\|''Allowed CORS Methods''\|''Allowed CORS Origins''\|''Anthropic Claude…''\|''Application favicon…''\|''Application logo…''\|''Application name''\|''Auto Logout Idle …''\|''Automatically block…''\|''Backup Frequency''\|''Bypass secret key''\|''Compress with gzip''\|''Concurrent Session…''\|''Contact email for…''\|''Current version of…''\|''Date display format''\|''DeepSeek API Key''\|''Default AI model''\|''Default language''\|''Default system…''\|''Default Telegram…''\|''Description text''\|''Email Driver \(smtp,…''\|''Enable 2FA''\|''Enable AI services''\|''Enable API…''\|''Enable automated…''\|''Enable Captcha''\|''Enable Cookie…''\|''Enable HTTP Strict…''\|''Enable Laravel…''\|''Enable maintenance…''\|''Enable Passkeys …''\|''Enable/Disable the…''\|''Encrypt Session Data''\|''Encryption \(tls/ssl…''\|''Encryption Key \(min…''\|''Environment…''\|''Estimated end time''\|''Excluded Tables …''\|''Execution Time''\|''Facebook App ID''\|''Facebook App Secret''\|''Footer text''\|''Force HTTPS…''\|''GitHub Client ID''\|''GitHub Client Secret''\|''Global IP Blacklist…''\|''Global Social Login…''\|''Google Client ID''\|''Google Client Secret''\|''Google Drive Client…''\|''Google Drive Folder…''\|''Google Drive…''\|''Google Gemini API…''\|''Help Center / FAQ…''\|''Hits threshold…''\|''Lockout Duration …''\|''Log Login Activity''\|''Maintenance…''\|''Maintenance message''\|''Maintenance page…''\|''Make PDP agreement…''\|''Max requests per…''\|''Maximum Login…''\|''Maximum Password…''\|''Minimum Password…''\|''Mistral AI API Key''\|''Notification Target…''\|''Notification…''\|''Notify upon Lockout''\|''Official company…''\|''Ollama Base URL''\|''Only allow 1 active…''\|''OpenAI GPT API Key''\|''OpenRouter API Key''\|''Password Reset Link…''\|''Password Validity …''\|''Prevent Password…''\|''Primary tagline''\|''Privacy Policy \(UU…''\|''reCAPTCHA Secret''\|''reCAPTCHA Site Key''\|''reCAPTCHA Version''\|''Remember Me…''\|''Remember Trusted…''\|''Require Lowercase…''\|''Require Numbers''\|''Require Symbols /…''\|''Require Uppercase…''\|''Retention Policy …''\|''Retry\-After seconds''\|''S3 Access Key''\|''S3 Bucket Name''\|''S3 Custom Endpoint …''\|''S3 Region''\|''S3 Secret Key''\|''SAP Application…''\|''SAP Client Number''\|''SAP Password''\|''SAP RFC Trace Level''\|''SAP Router string …''\|''SAP System Number''\|''SAP Username''\|''Secondary tagline''\|''Secure Cookie …''\|''Security Policy…''\|''Sender Display Name''\|''Sender Email Address''\|''Session Driver …''\|''Session Lifetime /…''\|''SMTP Host Server''\|''SMTP Password''\|''SMTP Port \(587/465…''\|''SMTP Username''\|''Storage Driver …''\|''System default…''\|''Telegram Bot Token''\|''Terms of Use Content''\|''Time display format…''\|''Toggle Facebook…''\|''Toggle GitHub OAuth…''\|''Toggle Google OAuth…''\|''Toggle notification…''\|''Whitelisted IP…''\|''xAI Grok API Key''\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 1
path: app/Services/SystemConfig/SystemConfigService.php
-
message: '#^Offset ''is_public'' on array\{type\: ''bool''\|''float''\|''image_path''\|''int''\|''string''\|''text'', group\: ''ai_config''\|''backups''\|''branding''\|''content_legal''\|''feature_flags''\|''ip_access''\|''login_security''\|''maintenance''\|''monitoring''\|''notifications''\|''password_policy''\|''regional''\|''sap_integration''\|''session_security'', is_public\: bool, default\: 0\|0\.7\|1\|5\|7\|8\|15\|30\|60\|64\|100\|120\|587\|2000\|3600\|''''\|''\*''\|''/auth/callback''\|''0''\|''00''\|''02\:00''\|''100''\|''Asia/Jakarta''\|''d/m/Y''\|''daily''\|''email''\|''en''\|''failed''\|''gpt''\|''gpt\-4o''\|''H\:i''\|''http\://localhost…''\|''Laravel''\|''LaravelBackups''\|''local''\|''noreply@example\.com''\|''Production''\|''redis''\|''Scheduled System…''\|''smtp''\|''System Notification''\|''The system is…''\|''tls''\|''us\-east\-1''\|''v2''\|bool\|null, description\: ''2FA Method''\|''About Us Content''\|''Active AI provider''\|''Admin IP Whitelist …''\|''AES\-256 Encryption''\|''AI max tokens''\|''AI temperature''\|''Allow Remember Me…''\|''Allowed CORS Headers''\|''Allowed CORS Methods''\|''Allowed CORS Origins''\|''Anthropic Claude…''\|''Application favicon…''\|''Application logo…''\|''Application name''\|''Auto Logout Idle …''\|''Automatically block…''\|''Backup Frequency''\|''Bypass secret key''\|''Compress with gzip''\|''Concurrent Session…''\|''Contact email for…''\|''Current version of…''\|''Date display format''\|''DeepSeek API Key''\|''Default AI model''\|''Default language''\|''Default system…''\|''Default Telegram…''\|''Description text''\|''Email Driver \(smtp,…''\|''Enable 2FA''\|''Enable AI services''\|''Enable API…''\|''Enable automated…''\|''Enable Captcha''\|''Enable Cookie…''\|''Enable HTTP Strict…''\|''Enable Laravel…''\|''Enable maintenance…''\|''Enable Passkeys …''\|''Enable/Disable the…''\|''Encrypt Session Data''\|''Encryption \(tls/ssl…''\|''Encryption Key \(min…''\|''Environment…''\|''Estimated end time''\|''Excluded Tables …''\|''Execution Time''\|''Facebook App ID''\|''Facebook App Secret''\|''Footer text''\|''Force HTTPS…''\|''GitHub Client ID''\|''GitHub Client Secret''\|''Global IP Blacklist…''\|''Global Social Login…''\|''Google Client ID''\|''Google Client Secret''\|''Google Drive Client…''\|''Google Drive Folder…''\|''Google Drive…''\|''Google Gemini API…''\|''Help Center / FAQ…''\|''Hits threshold…''\|''Lockout Duration …''\|''Log Login Activity''\|''Maintenance…''\|''Maintenance message''\|''Maintenance page…''\|''Make PDP agreement…''\|''Max requests per…''\|''Maximum Login…''\|''Maximum Password…''\|''Minimum Password…''\|''Mistral AI API Key''\|''Notification Target…''\|''Notification…''\|''Notify upon Lockout''\|''Official company…''\|''Ollama Base URL''\|''Only allow 1 active…''\|''OpenAI GPT API Key''\|''OpenRouter API Key''\|''Password Reset Link…''\|''Password Validity …''\|''Prevent Password…''\|''Primary tagline''\|''Privacy Policy \(UU…''\|''reCAPTCHA Secret''\|''reCAPTCHA Site Key''\|''reCAPTCHA Version''\|''Remember Me…''\|''Remember Trusted…''\|''Require Lowercase…''\|''Require Numbers''\|''Require Symbols /…''\|''Require Uppercase…''\|''Retention Policy …''\|''Retry\-After seconds''\|''S3 Access Key''\|''S3 Bucket Name''\|''S3 Custom Endpoint …''\|''S3 Region''\|''S3 Secret Key''\|''SAP Application…''\|''SAP Client Number''\|''SAP Password''\|''SAP RFC Trace Level''\|''SAP Router string …''\|''SAP System Number''\|''SAP Username''\|''Secondary tagline''\|''Secure Cookie …''\|''Security Policy…''\|''Sender Display Name''\|''Sender Email Address''\|''Session Driver …''\|''Session Lifetime /…''\|''SMTP Host Server''\|''SMTP Password''\|''SMTP Port \(587/465…''\|''SMTP Username''\|''Storage Driver …''\|''System default…''\|''Telegram Bot Token''\|''Terms of Use Content''\|''Time display format…''\|''Toggle Facebook…''\|''Toggle GitHub OAuth…''\|''Toggle Google OAuth…''\|''Toggle notification…''\|''Whitelisted IP…''\|''xAI Grok API Key''\} on left side of \?\? always exists and is not nullable\.$#'
identifier: nullCoalesce.offset
count: 2
path: app/Services/SystemConfig/SystemConfigService.php
-
message: '#^Trait App\\Traits\\HasAutoCode is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: app/Traits/HasAutoCode.php
+20
View File
@@ -0,0 +1,20 @@
includes:
- vendor/larastan/larastan/extension.neon
- phpstan-baseline.neon
parameters:
level: 5
paths:
- app
excludePaths:
- app/Console/Commands
ignoreErrors:
# Spatie permission magic
- '#Call to an undefined method App\\Models\\User::permission\(\)#'
# Eloquent dynamic property/scope warnings on Spatie Model extensions
- '#Access to an undefined property Spatie\\#'
tmpDir: storage/framework/phpstan
parallel:
processTimeout: 300.0
maximumNumberOfProcesses: 4
reportUnmatchedIgnoredErrors: false
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array" force="true"/>
<env name="DB_DATABASE" value="testing"/>
<env name="MAIL_MAILER" value="array" force="true"/>
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
<env name="SESSION_DRIVER" value="array" force="true"/>
<env name="PULSE_ENABLED" value="false" force="true"/>
<env name="TELESCOPE_ENABLED" value="false" force="true"/>
<env name="NIGHTWATCH_ENABLED" value="false" force="true"/>
</php>
</phpunit>
Executable
+687
View File
@@ -0,0 +1,687 @@
#!/usr/bin/env bash
UNAMEOUT="$(uname -s)"
# Verify operating system is supported...
case "${UNAMEOUT}" in
Linux*) MACHINE=linux;;
Darwin*) MACHINE=mac;;
*) MACHINE="UNKNOWN"
esac
if [ "$MACHINE" == "UNKNOWN" ]; then
echo "Unsupported operating system [$(uname -s)]. Laravel Sail supports macOS, Linux, and Windows (WSL2)." >&2
exit 1
fi
# Determine if stdout is a terminal...
if test -t 1; then
# Determine if colors are supported...
ncolors=$(tput colors)
if test -n "$ncolors" && test "$ncolors" -ge 8; then
BOLD="$(tput bold)"
YELLOW="$(tput setaf 3)"
GREEN="$(tput setaf 2)"
NC="$(tput sgr0)"
fi
fi
# Function that prints the available commands...
function display_help {
echo "Laravel Sail"
echo
echo "${YELLOW}Usage:${NC}" >&2
echo " sail COMMAND [options] [arguments]"
echo
echo "Unknown commands are passed to the docker-compose binary."
echo
echo "${YELLOW}docker-compose Commands:${NC}"
echo " ${GREEN}sail up${NC} Start the application"
echo " ${GREEN}sail up -d${NC} Start the application in the background"
echo " ${GREEN}sail stop${NC} Stop the application"
echo " ${GREEN}sail restart${NC} Restart the application"
echo " ${GREEN}sail ps${NC} Display the status of all containers"
echo
echo "${YELLOW}Artisan Commands:${NC}"
echo " ${GREEN}sail artisan ...${NC} Run an Artisan command"
echo " ${GREEN}sail artisan queue:work${NC}"
echo
echo "${YELLOW}PHP Commands:${NC}"
echo " ${GREEN}sail php ...${NC} Run a snippet of PHP code"
echo " ${GREEN}sail php -v${NC}"
echo
echo "${YELLOW}Composer Commands:${NC}"
echo " ${GREEN}sail composer ...${NC} Run a Composer command"
echo " ${GREEN}sail composer require laravel/sanctum${NC}"
echo
echo "${YELLOW}Node Commands:${NC}"
echo " ${GREEN}sail node ...${NC} Run a Node command"
echo " ${GREEN}sail node --version${NC}"
echo
echo "${YELLOW}NPM Commands:${NC}"
echo " ${GREEN}sail npm ...${NC} Run a npm command"
echo " ${GREEN}sail npx${NC} Run a npx command"
echo " ${GREEN}sail npm run prod${NC}"
echo
echo "${YELLOW}PNPM Commands:${NC}"
echo " ${GREEN}sail pnpm ...${NC} Run a pnpm command"
echo " ${GREEN}sail pnpx${NC} Run a pnpx command"
echo " ${GREEN}sail pnpm run prod${NC}"
echo
echo "${YELLOW}Yarn Commands:${NC}"
echo " ${GREEN}sail yarn ...${NC} Run a Yarn command"
echo " ${GREEN}sail yarn run prod${NC}"
echo
echo "${YELLOW}Bun Commands:${NC}"
echo " ${GREEN}sail bun ...${NC} Run a bun command"
echo " ${GREEN}sail bunx${NC} Run a bunx command"
echo " ${GREEN}sail bun run prod${NC}"
echo
echo "${YELLOW}Database Commands:${NC}"
echo " ${GREEN}sail mysql${NC} Start a MySQL CLI session within the 'mysql' container"
echo " ${GREEN}sail mariadb${NC} Start a MySQL CLI session within the 'mariadb' container"
echo " ${GREEN}sail psql${NC} Start a PostgreSQL CLI session within the 'pgsql' container"
echo " ${GREEN}sail mongodb${NC} Start a Mongo Shell session within the 'mongodb' container"
echo " ${GREEN}sail redis${NC} Start a Redis CLI session within the 'redis' container"
echo " ${GREEN}sail valkey${NC} Start a Valkey CLI session within the 'valkey' container"
echo
echo "${YELLOW}Debugging:${NC}"
echo " ${GREEN}sail debug ...${NC} Run an Artisan command in debug mode"
echo " ${GREEN}sail debug queue:work${NC}"
echo
echo "${YELLOW}Running Tests:${NC}"
echo " ${GREEN}sail test${NC} Run the PHPUnit tests via the Artisan test command"
echo " ${GREEN}sail phpunit ...${NC} Run PHPUnit"
echo " ${GREEN}sail pest ...${NC} Run Pest"
echo " ${GREEN}sail pint ...${NC} Run Pint"
echo " ${GREEN}sail dusk${NC} Run the Dusk tests (Requires the laravel/dusk package)"
echo " ${GREEN}sail dusk:fails${NC} Re-run previously failed Dusk tests (Requires the laravel/dusk package)"
echo
echo "${YELLOW}Container CLI:${NC}"
echo " ${GREEN}sail shell${NC} Start a shell session within the application container"
echo " ${GREEN}sail bash${NC} Alias for 'sail shell'"
echo " ${GREEN}sail root-shell${NC} Start a root shell session within the application container"
echo " ${GREEN}sail root-bash${NC} Alias for 'sail root-shell'"
echo " ${GREEN}sail tinker${NC} Start a new Laravel Tinker session"
echo
echo "${YELLOW}Sharing:${NC}"
echo " ${GREEN}sail share${NC} Share the application publicly via a temporary URL"
echo " ${GREEN}sail open${NC} Open the site in your browser"
echo
echo "${YELLOW}Binaries:${NC}"
echo " ${GREEN}sail bin ...${NC} Run Composer binary scripts from the vendor/bin directory"
echo " ${GREEN}sail run ...${NC} Run a command within the application container"
echo
echo "${YELLOW}Customization:${NC}"
echo " ${GREEN}sail artisan sail:publish${NC} Publish the Sail configuration files"
echo " ${GREEN}sail build --no-cache${NC} Rebuild all of the Sail containers"
exit 1
}
# Proxy the "help" command...
if [ $# -gt 0 ]; then
if [ "$1" == "help" ] || [ "$1" == "-h" ] || [ "$1" == "-help" ] || [ "$1" == "--help" ]; then
display_help
fi
else
display_help
fi
# Source the ".env" file so Laravel's environment variables are available...
# shellcheck source=/dev/null
if [ -n "$APP_ENV" ] && [ -f ./.env."$APP_ENV" ]; then
source ./.env."$APP_ENV";
elif [ -f ./.env ]; then
source ./.env;
fi
# Define environment variables...
export APP_PORT=${APP_PORT:-80}
export APP_SERVICE=${APP_SERVICE:-"laravel.test"}
export APP_USER=${APP_USER:-"sail"}
export DB_PORT=${DB_PORT:-3306}
export WWWUSER=${WWWUSER:-$UID}
export WWWGROUP=${WWWGROUP:-$(id -g)}
export SAIL_FILES=${SAIL_FILES:-""}
export SAIL_SHARE_DASHBOARD=${SAIL_SHARE_DASHBOARD:-4040}
export SAIL_SHARE_SERVER_HOST=${SAIL_SHARE_SERVER_HOST:-"laravel-sail.site"}
export SAIL_SHARE_SERVER_PORT=${SAIL_SHARE_SERVER_PORT:-8080}
export SAIL_SHARE_SUBDOMAIN=${SAIL_SHARE_SUBDOMAIN:-""}
export SAIL_SHARE_DOMAIN=${SAIL_SHARE_DOMAIN:-"$SAIL_SHARE_SERVER_HOST"}
export SAIL_SHARE_SERVER=${SAIL_SHARE_SERVER:-""}
export SAIL_DOCKER_BINARY=${SAIL_DOCKER_BINARY:-"docker"}
# Function that outputs Sail is not running...
function sail_is_not_running {
echo "${BOLD}Sail is not running.${NC}" >&2
echo "" >&2
echo "${BOLD}You may Sail using the following commands:${NC} './vendor/bin/sail up' or './vendor/bin/sail up -d'" >&2
exit 1
}
# AI agent environment variables to forward into containers...
AGENT_ENV_VARS=(
AI_AGENT
CLAUDECODE
CLAUDE_CODE
CURSOR_TRACE_ID
CURSOR_AGENT
GEMINI_CLI
CODEX_SANDBOX
CODEX_CI
CODEX_THREAD_ID
AUGMENT_AGENT
OPENCODE_CLIENT
OPENCODE
AMP_CURRENT_THREAD_ID
REPL_ID
ANTIGRAVITY_AGENT
COPILOT_MODEL
COPILOT_CLI
PI_CODING_AGENT
)
function forward_agent_env() {
for VAR in "${AGENT_ENV_VARS[@]}"; do
if [ -n "${!VAR:-}" ]; then
ARGS+=(-e "${VAR}=${!VAR}")
fi
done
}
# Define Docker Compose command prefix...
if ${SAIL_DOCKER_BINARY} compose &> /dev/null; then
COMPOSE_CMD=(${SAIL_DOCKER_BINARY} compose)
else
COMPOSE_CMD=(${SAIL_DOCKER_BINARY}-compose)
fi
if [ -n "$SAIL_FILES" ]; then
# Convert SAIL_FILES to an array...
IFS=':' read -ra SAIL_FILES <<< "$SAIL_FILES"
for FILE in "${SAIL_FILES[@]}"; do
if [ -f "$FILE" ]; then
COMPOSE_CMD+=(-f "$FILE")
else
echo "${BOLD}Unable to find Docker Compose file: '${FILE}'${NC}" >&2
exit 1
fi
done
fi
EXEC="yes"
if [ -z "$SAIL_SKIP_CHECKS" ]; then
# Ensure that Docker is running...
if ! ${SAIL_DOCKER_BINARY} info > /dev/null 2>&1; then
echo "${BOLD}Docker or Podman is not running.${NC}" >&2
exit 1
fi
# Determine if Sail is currently up...
if "${COMPOSE_CMD[@]}" ps "$APP_SERVICE" 2>&1 | grep 'Exit\|exited'; then
echo "${BOLD}Shutting down old Sail processes...${NC}" >&2
"${COMPOSE_CMD[@]}" down > /dev/null 2>&1
EXEC="no"
elif [ -z "$("${COMPOSE_CMD[@]}" ps -q)" ]; then
EXEC="no"
fi
fi
ARGS=()
# Proxy PHP commands to the "php" binary on the application container...
if [ "$1" == "php" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" "php")
else
sail_is_not_running
fi
# Proxy vendor binary commands on the application container...
elif [ "$1" == "bin" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
CMD=$1
shift 1
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" ./vendor/bin/"$CMD")
else
sail_is_not_running
fi
# Proxy commands on the application container...
elif [ "$1" == "run" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
CMD=$1
shift 1
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" "$CMD")
else
sail_is_not_running
fi
# Proxy docker-compose commands to the docker-compose binary on the application container...
elif [ "$1" == "docker-compose" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" "${COMPOSE_CMD[@]}")
else
sail_is_not_running
fi
# Proxy Composer commands to the "composer" binary on the application container...
elif [ "$1" == "composer" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" "composer")
else
sail_is_not_running
fi
# Proxy Artisan commands to the "artisan" binary on the application container...
elif [ "$1" == "artisan" ] || [ "$1" == "art" ] || [ "$1" == "a" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php artisan)
else
sail_is_not_running
fi
# Proxy the "debug" command to the "php artisan" binary on the application container with xdebug enabled...
elif [ "$1" == "debug" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER" -e XDEBUG_TRIGGER=1)
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php artisan)
else
sail_is_not_running
fi
# Proxy the "test" command to the "php artisan test" Artisan command...
elif [ "$1" == "test" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php artisan test)
else
sail_is_not_running
fi
# Proxy the "phpunit" command to "php vendor/bin/phpunit"...
elif [ "$1" == "phpunit" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php vendor/bin/phpunit)
else
sail_is_not_running
fi
# Proxy the "pest" command to "php vendor/bin/pest"...
elif [ "$1" == "pest" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php vendor/bin/pest)
else
sail_is_not_running
fi
# Proxy the "pint" command to "php vendor/bin/pint"...
elif [ "$1" == "pint" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php vendor/bin/pint)
else
sail_is_not_running
fi
# Proxy the "dusk" command to the "php artisan dusk" Artisan command...
elif [ "$1" == "dusk" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=(-e "APP_URL=http://${APP_SERVICE}")
ARGS+=(-e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub")
ARGS+=("$APP_SERVICE" php artisan dusk)
else
sail_is_not_running
fi
# Proxy the "dusk:fails" command to the "php artisan dusk:fails" Artisan command...
elif [ "$1" == "dusk:fails" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=(-e "APP_URL=http://${APP_SERVICE}")
ARGS+=(-e "DUSK_DRIVER_URL=http://selenium:4444/wd/hub")
ARGS+=("$APP_SERVICE" php artisan dusk:fails)
else
sail_is_not_running
fi
# Initiate a Laravel Tinker session within the application container...
elif [ "$1" == "tinker" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" php artisan tinker)
else
sail_is_not_running
fi
# Proxy Node commands to the "node" binary on the application container...
elif [ "$1" == "node" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" node)
else
sail_is_not_running
fi
# Proxy NPM commands to the "npm" binary on the application container...
elif [ "$1" == "npm" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" npm)
else
sail_is_not_running
fi
# Proxy NPX commands to the "npx" binary on the application container...
elif [ "$1" == "npx" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" npx)
else
sail_is_not_running
fi
# Proxy PNPM commands to the "pnpm" binary on the application container...
elif [ "$1" == "pnpm" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" pnpm)
else
sail_is_not_running
fi
# Proxy PNPX commands to the "pnpx" binary on the application container...
elif [ "$1" == "pnpx" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" pnpx)
else
sail_is_not_running
fi
# Proxy Yarn commands to the "yarn" binary on the application container...
elif [ "$1" == "yarn" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" yarn)
else
sail_is_not_running
fi
# Proxy Bun commands to the "bun" binary on the application container...
elif [ "$1" == "bun" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" bun)
else
sail_is_not_running
fi
# Proxy Bun X commands to the "bunx" binary on the application container...
elif [ "$1" == "bunx" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" bunx)
else
sail_is_not_running
fi
# Initiate a MySQL CLI terminal session within the "mysql" container...
elif [ "$1" == "mysql" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(mysql bash -c)
ARGS+=("MYSQL_PWD=\${MYSQL_PASSWORD} mysql -u \${MYSQL_USER} \${MYSQL_DATABASE} \${MYSQL_EXTRA_OPTIONS}")
else
sail_is_not_running
fi
# Initiate a MySQL CLI terminal session within the "mariadb" container...
elif [ "$1" == "mariadb" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(mariadb bash -c)
ARGS+=("MYSQL_PWD=\${MYSQL_PASSWORD} mariadb -u \${MYSQL_USER} \${MYSQL_DATABASE}")
else
sail_is_not_running
fi
# Initiate a PostgreSQL CLI terminal session within the "pgsql" container...
elif [ "$1" == "psql" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(pgsql bash -c)
ARGS+=("PGPASSWORD=\${PGPASSWORD} psql -U \${POSTGRES_USER} \${POSTGRES_DB}")
else
sail_is_not_running
fi
# Initiate a Bash shell within the application container...
elif [ "$1" == "shell" ] || [ "$1" == "bash" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u "$APP_USER")
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" bash)
else
sail_is_not_running
fi
# Initiate a root user Bash shell within the application container...
elif [ "$1" == "root-shell" ] || [ "$1" == "root-bash" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec -u root)
[ ! -t 0 ] && ARGS+=(-T)
forward_agent_env
ARGS+=("$APP_SERVICE" bash)
else
sail_is_not_running
fi
# Initiate a MongoDB Shell within the "mongodb" container...
elif [ "$1" == "mongodb" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(mongodb mongosh --port "${FORWARD_MONGODB_PORT:-27017}" --username "$MONGODB_USERNAME" --password "$MONGODB_PASSWORD" --authenticationDatabase admin)
else
sail_is_not_running
fi
# Initiate a Redis CLI terminal session within the "redis" container...
elif [ "$1" == "redis" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(redis redis-cli)
else
sail_is_not_running
fi
# Initiate a Valkey CLI terminal session within the "valkey" container...
elif [ "$1" == "valkey" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
ARGS+=(exec)
[ ! -t 0 ] && ARGS+=(-T)
ARGS+=(valkey valkey-cli)
else
sail_is_not_running
fi
# Share the site...
elif [ "$1" == "share" ]; then
shift 1
if [ "$EXEC" == "yes" ]; then
${SAIL_DOCKER_BINARY} run --init --rm --add-host=host.docker.internal:host-gateway -p "$SAIL_SHARE_DASHBOARD":4040 -t beyondcodegmbh/expose-server:latest share http://host.docker.internal:"$APP_PORT" \
--server-host="$SAIL_SHARE_SERVER_HOST" \
--server-port="$SAIL_SHARE_SERVER_PORT" \
--auth="$SAIL_SHARE_TOKEN" \
--server="$SAIL_SHARE_SERVER" \
--subdomain="$SAIL_SHARE_SUBDOMAIN" \
--domain="$SAIL_SHARE_DOMAIN" \
"$@"
exit
else
sail_is_not_running
fi
# Open the site...
elif [ "$1" == "open" ]; then
shift 1
if command -v open &>/dev/null; then
OPEN="open"
elif command -v xdg-open &>/dev/null; then
OPEN="xdg-open"
else
echo "Neither open nor xdg-open is available. Exiting."
exit 1
fi
if [ "$EXEC" == "yes" ]; then
if [[ -n "$APP_PORT" && "$APP_PORT" != "80" ]]; then
FULL_URL="${APP_URL}:${APP_PORT}"
else
FULL_URL="$APP_URL"
fi
$OPEN "$FULL_URL"
exit
else
sail_is_not_running
fi
fi
# Run Docker Compose with the defined arguments...
"${COMPOSE_CMD[@]}" "${ARGS[@]}" "$@"