From 2311767e9fc943984e6fa7e46994b5f579604b2b Mon Sep 17 00:00:00 2001 From: debesocial Date: Thu, 21 May 2026 16:05:53 +0700 Subject: [PATCH] chore: update gitignore and add root docs and tooling --- .gitignore | 4 + CHANGELOG.md | 153 ++++++++++ DEPLOYMENT_GUIDE.md | 478 +++++++++++++++++++++++++++++ SECURITY.md | 127 ++++++++ TECH_STACK.md | 327 ++++++++++++++++++++ USER_GUIDE.md | 262 ++++++++++++++++ check-production.sh | 95 ++++++ check-sap.php | 64 ++++ phpstan-baseline.neon | 331 ++++++++++++++++++++ phpstan.neon | 20 ++ phpunit.xml | 34 +++ sail | 687 ++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 2582 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 DEPLOYMENT_GUIDE.md create mode 100644 SECURITY.md create mode 100644 TECH_STACK.md create mode 100644 USER_GUIDE.md create mode 100755 check-production.sh create mode 100644 check-sap.php create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100755 sail diff --git a/.gitignore b/.gitignore index f8e99bc..6336b84 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ yarn-error.log* !/storage/framework/testing/.gitignore !/storage/framework/phpstan/.gitignore !/storage/logs/.gitignore + +# Subproject ignores +/Project/storage/ +/Project/**/*.log diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..90f1d58 --- /dev/null +++ b/CHANGELOG.md @@ -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). diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..00ea918 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -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 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/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 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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2f04a86 --- /dev/null +++ b/SECURITY.md @@ -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 `` 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 +``` diff --git a/TECH_STACK.md b/TECH_STACK.md new file mode 100644 index 0000000..6b42bb5 --- /dev/null +++ b/TECH_STACK.md @@ -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) │ + └─────────────┘ +``` diff --git a/USER_GUIDE.md b/USER_GUIDE.md new file mode 100644 index 0000000..e252c79 --- /dev/null +++ b/USER_GUIDE.md @@ -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 | diff --git a/check-production.sh b/check-production.sh new file mode 100755 index 0000000..ed2e3a9 --- /dev/null +++ b/check-production.sh @@ -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}" diff --git a/check-sap.php b/check-sap.php new file mode 100644 index 0000000..572ae84 --- /dev/null +++ b/check-sap.php @@ -0,0 +1,64 @@ +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\\:\: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\ 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 diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..176523b --- /dev/null +++ b/phpstan.neon @@ -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 diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..81be5e7 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + diff --git a/sail b/sail new file mode 100755 index 0000000..9c0e77f --- /dev/null +++ b/sail @@ -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[@]}" "$@"