From e4932f77959bd0cb62da92b40aca86161be3e19a Mon Sep 17 00:00:00 2001
From: dyzulk <66510723+dyzulk@users.noreply.github.com>
Date: Tue, 6 Jan 2026 11:06:45 +0700
Subject: [PATCH] feat: implement separate CA database with documentation and
standardized workflows
---
.agent/workflows/database-guidelines.md | 34 +++++
.agent/workflows/deployment.md | 51 +++++++
.agent/workflows/manage-env.md | 48 +++++++
.env.example.for.local | 128 ++++++++++++++++++
.env.example.for.production | 128 ++++++++++++++++++
.gitignore | 3 +-
.../Commands/MigrateCaCertificates.php | 80 +++++++++++
app/Models/CaCertificate.php | 2 +
config/database.php | 20 +++
...01_create_ca_certificates_table_new_db.php | 40 ++++++
scripts/deploy-webhook.example.sh | 121 +++++++++++++++++
11 files changed, 653 insertions(+), 2 deletions(-)
create mode 100644 .agent/workflows/database-guidelines.md
create mode 100644 .agent/workflows/deployment.md
create mode 100644 .agent/workflows/manage-env.md
create mode 100644 .env.example.for.local
create mode 100644 .env.example.for.production
create mode 100644 app/Console/Commands/MigrateCaCertificates.php
create mode 100644 database/migrations/2026_01_06_000001_create_ca_certificates_table_new_db.php
create mode 100644 scripts/deploy-webhook.example.sh
diff --git a/.agent/workflows/database-guidelines.md b/.agent/workflows/database-guidelines.md
new file mode 100644
index 0000000..e36db7a
--- /dev/null
+++ b/.agent/workflows/database-guidelines.md
@@ -0,0 +1,34 @@
+---
+description: Panduan Manajemen Database (Multi-Database Architecture)
+---
+
+# Aturan & Panduan Database
+
+Proyek ini menggunakan arsitektur **Multi-Database** untuk memisahkan data User/App dengan data High-Security (Certificate Authority).
+
+## Arsitektur
+
+1. **Main Database Connection (`mysql`)**
+ * **Kegunaan**: Menyimpan data aplikasi umum (`users`, `tickets`, `certificates` (leaf), dll).
+ * **Reset Policy**: Boleh di-reset saat development (`php artisan migrate:fresh --seed`).
+ * **Dependency**: Terikat dengan logic aplikasi utama.
+
+2. **CA Database Connection (`mysql_ca`)**
+ * **Kegunaan**: KHUSUS untuk `ca_certificates` (Root & Intermediate CA).
+ * **Reset Policy**: **DILARANG RESET** sembarangan. Command `migrate:fresh` default TIDAK akan menyentuh database ini.
+ * **Driver**: Menggunakan `mysql` di Production (sama seperti Main DB), bukan SQLite atau D1 (kecuali ada instruksi spesifik).
+
+## Aturan Migrasi
+
+1. **Pembuatan Tabel Baru**:
+ * Tentukan tabel masuk ke kategori mana (App vs CA).
+ * Jika CA, gunakan `Schema::connection('mysql_ca')->create(...)`.
+ * Jika App, gunakan `Schema::create(...)` biasa.
+
+2. **Data Safety**:
+ * Sebelum menjalankan query raw atau operasi destructive, pastikan koneksi yang dipilih benar.
+ * Gunakan command `php artisan ca:migrate-data` hanya jika perlu memindahkan data antar database.
+
+## Cloudflare D1
+* Saat ini D1 **TIDAK DIGUNAKAN** untuk kompatibilitas penuh dengan server berbasis VPS/Hosting standar.
+* Jangan mengusulkan migrasi ke D1 kecuali infrastruktur berpindah ke Cloudflare Workers sepenuhnya.
diff --git a/.agent/workflows/deployment.md b/.agent/workflows/deployment.md
new file mode 100644
index 0000000..4887f10
--- /dev/null
+++ b/.agent/workflows/deployment.md
@@ -0,0 +1,51 @@
+---
+description: SOP Deployment (CI/CD via aaPanel)
+---
+
+# Alur Kerja Deployment
+
+Proyek ini menggunakan **CI/CD Otomatis** via aaPanel Webhook yang terintegrasi dengan GitHub/Git.
+
+## 1. Automated Deployment (CI/CD)
+
+Setiap kali Anda melakukan push ke branch `main`, script webhook di server akan berjalan.
+**Apa yang dilakukan script otomatis:**
+1. `git pull origin main`
+2. `composer install` & `npm install` + `vite build`
+3. **Update Config:** Mengcopy isi `.env.production.editable` ke `.env` (Pastikan file editable sudah benar di repo!).
+4. `php artisan migrate --force` (Main & CA Database).
+5. `php artisan optimize`.
+
+**Script Reference:**
+* **Repo (Public):** `scripts/deploy-webhook.example.sh` (Template aman, gunakan ini untuk copy-paste ke aaPanel lalu edit manual).
+* **Local (Private):** `scripts/deploy-webhook.local.sh` (Backup pribadi Anda dengan path asli, ter-ignore oleh git).
+
+## 2. Manual Pre-Requisites (Sebelum Push)
+
+Sebelum Anda push code, pastikan:
+1. **Environment Variables**:
+ * Jika ada perubahan config, update `.env.production.editable`.
+ * Ingat: Script akan menimpa `.env` server dengan isi `.env.production.editable`.
+2. **Database**:
+ * Jika membuat DB baru (seperti kasus CA ini), pastikan database fisik sudah dibuat di server MySQL (`CREATE DATABASE ...`).
+
+## 3. Manual Post-Deployment (Intervensi Khusus)
+
+Script CI/CD tidak menangani edge-cases. Anda perlu masuk ke server (SSH) untuk kasus berikut:
+
+1. **Data Migration Khusus**:
+ * Kasus: Memisahkan table CA ke database baru.
+ * Action: Login SSH, lalu jalankan:
+ ```bash
+ cd /www/wwwroot/trustlab-api-ftp/trustlab-api.dyzulk.com
+ php artisan ca:migrate-data
+ ```
+
+2. **Rollback**:
+ * Jika deploy gagal total, Anda mungkin perlu restore backup database manual via aaPanel atau `php artisan migrate:rollback`.
+
+## 4. Platform Lain (Non-aaPanel)
+Jika berpindah dari aaPanel, adaptasi script `scripts/deploy-webhook.example.sh`:
+* Ganti Path project (`PROJECT_PATH`).
+* Ganti Path PHP Binary (`PHP_BIN`).
+* Ganti mekanisme trigger (misal gunakan GitHub Actions, Jenkins, atau Laravel Forge).
diff --git a/.agent/workflows/manage-env.md b/.agent/workflows/manage-env.md
new file mode 100644
index 0000000..568c7c7
--- /dev/null
+++ b/.agent/workflows/manage-env.md
@@ -0,0 +1,48 @@
+---
+description: Memahami dan Mengelola Environment Variables (5-File System)
+---
+
+# Aturan Manajemen Environment Variables
+
+Proyek ini menggunakan sistem **5-File Environment** yang ketat untuk mencegah kesalahan konfigurasi produksi. AI dan Developer Wajib mengikuti aturan ini.
+
+## Struktur File
+
+1. **`.env`** (Local Development)
+ * Digunakan untuk pengembangan visual/lokal.
+ * Berisi kredensial lokal (localhost, root, dll).
+ * **Aturan:** Menjadi acuan utama *struktur* dan *urutan* key untuk file lainnya.
+
+2. **`.env.example.for.local`** (Template Local)
+ * Template untuk developer lain.
+ * Struktur HARUS sama persis dengan `.env`.
+ * Value kosong atau default aman.
+
+3. **`.env.example.for.production`** (Template Production)
+ * Gambaran konfigurasi produksi.
+ * Struktur HARUS sama persis dengan `.env`.
+ * Value disesuaikan untuk konteks produksi (misal `APP_ENV=production`, `APP_DEBUG=false`).
+
+4. **`.env.production.editable`** (Staging/Pre-Production)
+ * File ini berisi konfigurasi produksi yang *siap* untuk diedit/standardisasi.
+ * **CRITICAL:** Struktur dan urutan key HARUS 100% sama dengan `.env`.
+ * Berisi kredensial RILL/ASLI dari server produksi.
+
+5. **`.env.production.soft.copy`** (Snapshot Server - **READ ONLY**)
+ * Merupakan salinan langsung dari server saat ini.
+ * **DILARANG EDIT** file ini kecuali server aktual telah berubah.
+ * File ini digunakan sebagai validasi/referensi state server sekarang.
+ * Jangan menambahkan config baru di sini sebelum server di-update.
+
+## Workflow Perubahan Environment
+
+Jika Anda perlu menambahkan Variable baru (misal `DB_CA_...`):
+
+1. **Tambahkan di `.env`** lokal terlebih dahulu.
+2. **Standardisasi urutan** di `.env.production.editable` (copy struktur `.env`, lalu isi value produksi).
+3. **Update Template** `.env.example.for.local` dan `.env.example.for.production`.
+4. **JANGAN SENTUH** `.env.production.soft.copy` (biarkan apa adanya sampai deployment selesai dan snapshot baru diambil).
+
+## Prompting AI
+Untuk memastikan AI mengerti konteks ini, mintalah:
+> "Baca aturan environment di `.agent/workflows/manage-env.md` sebelum melakukan perubahan pada file .env"
diff --git a/.env.example.for.local b/.env.example.for.local
new file mode 100644
index 0000000..b408530
--- /dev/null
+++ b/.env.example.for.local
@@ -0,0 +1,128 @@
+# This .env for example local development
+APP_NAME=TrustLab
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://localhost:8000
+
+APP_LOCALE=en
+APP_FALLBACK_LOCALE=en
+APP_FAKER_LOCALE=en_US
+
+APP_MAINTENANCE_DRIVER=file
+# APP_MAINTENANCE_STORE=database
+
+# PHP_CLI_SERVER_WORKERS=4
+
+BCRYPT_ROUNDS=12
+
+LOG_CHANNEL=stack
+LOG_STACK=single
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=sqlite
+# DB_HOST=127.0.0.1
+# DB_PORT=3306
+# DB_DATABASE=laravel
+# DB_USERNAME=root
+# DB_PASSWORD=
+
+# CA DB Connection
+DB_CA_CONNECTION=mysql_ca
+DB_CA_HOST=127.0.0.1
+DB_CA_PORT=3306
+DB_CA_DATABASE=trustlab_ca
+DB_CA_USERNAME=root
+DB_CA_PASSWORD=
+
+SESSION_DRIVER=database
+SESSION_LIFETIME=120
+SESSION_ENCRYPT=false
+SESSION_PATH=/
+SESSION_DOMAIN=localhost
+
+SANCTUM_STATEFUL_DOMAINS=localhost:3000,127.0.0.1:3000
+
+BROADCAST_CONNECTION=reverb
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=database
+
+CACHE_STORE=database
+# CACHE_PREFIX=
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_CLIENT=phpredis
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=lab.dyzulk.com
+MAIL_PORT=587
+MAIL_USERNAME=noreply@lab.dyzulk.com
+MAIL_PASSWORD=
+MAIL_ENCRYPTION=tls
+MAIL_FROM_ADDRESS="noreply@lab.dyzulk.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+MAIL_SUPPORT_MAILER=smtp
+MAIL_SUPPORT_HOST=lab.dyzulk.com
+MAIL_SUPPORT_PORT=587
+MAIL_SUPPORT_USERNAME=support@lab.dyzulk.com
+MAIL_SUPPORT_PASSWORD=
+MAIL_SUPPORT_ENCRYPTION=tls
+MAIL_SUPPORT_FROM_ADDRESS="support@lab.dyzulk.com"
+MAIL_SUPPORT_FROM_NAME="${APP_NAME} Support"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET=
+R2_PRIVATE_BUCKET=
+R2_ENDPOINT=
+R2_URL=
+
+VITE_APP_NAME="${APP_NAME}"
+
+FRONTEND_URL=http://localhost:3000
+
+BROADCAST_CONNECTION=reverb
+
+REVERB_APP_ID=
+REVERB_APP_KEY=
+REVERB_APP_SECRET=
+REVERB_HOST="localhost"
+REVERB_PORT=8080
+REVERB_SCHEME=http
+
+VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
+VITE_REVERB_HOST="${REVERB_HOST}"
+VITE_REVERB_PORT="${REVERB_PORT}"
+VITE_REVERB_SCHEME="${REVERB_SCHEME}"
+
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+GITHUB_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+GITHUB_DEV_CLIENT_ID=
+GITHUB_DEV_CLIENT_SECRET=
+GITHUB_DEV_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+GITHUB_PROD_CLIENT_ID=
+GITHUB_PROD_CLIENT_SECRET=
+GITHUB_PROD_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+GOOGLE_REDIRECT_URI=${APP_URL}/api/auth/google/callback
+
+TELEGRAM_BOT_TOKEN=
+TELEGRAM_CHAT_ID=
\ No newline at end of file
diff --git a/.env.example.for.production b/.env.example.for.production
new file mode 100644
index 0000000..bf3de84
--- /dev/null
+++ b/.env.example.for.production
@@ -0,0 +1,128 @@
+# This .env for example production
+APP_NAME=TrustLab
+APP_ENV=production
+APP_KEY=
+APP_DEBUG=false
+APP_URL=https://trustlab-api.dyzulk.com
+
+APP_LOCALE=en
+APP_FALLBACK_LOCALE=en
+APP_FAKER_LOCALE=en_US
+
+APP_MAINTENANCE_DRIVER=file
+# APP_MAINTENANCE_STORE=database
+
+# PHP_CLI_SERVER_WORKERS=4
+
+BCRYPT_ROUNDS=12
+
+LOG_CHANNEL=stack
+LOG_STACK=single
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=error
+
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=trustlab
+DB_USERNAME=trustlab
+DB_PASSWORD=
+
+# CA DB Connection
+DB_CA_CONNECTION=mysql_ca
+DB_CA_HOST=127.0.0.1
+DB_CA_PORT=3306
+DB_CA_DATABASE=trustlab_ca
+DB_CA_USERNAME=
+DB_CA_PASSWORD=
+
+SESSION_DRIVER=database
+SESSION_LIFETIME=120
+SESSION_ENCRYPT=false
+SESSION_PATH=/
+SESSION_DOMAIN=.dyzulk.com
+
+SANCTUM_STATEFUL_DOMAINS=trustlab.dyzulk.com
+
+BROADCAST_CONNECTION=reverb
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=database
+
+CACHE_STORE=database
+# CACHE_PREFIX=
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_CLIENT=phpredis
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=lab.dyzulk.com
+MAIL_PORT=587
+MAIL_USERNAME=noreply@lab.dyzulk.com
+MAIL_PASSWORD=
+MAIL_ENCRYPTION=tls
+MAIL_FROM_ADDRESS="noreply@lab.dyzulk.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+MAIL_SUPPORT_MAILER=smtp
+MAIL_SUPPORT_HOST=lab.dyzulk.com
+MAIL_SUPPORT_PORT=587
+MAIL_SUPPORT_USERNAME=support@lab.dyzulk.com
+MAIL_SUPPORT_PASSWORD=
+MAIL_SUPPORT_ENCRYPTION=tls
+MAIL_SUPPORT_FROM_ADDRESS="support@lab.dyzulk.com"
+MAIL_SUPPORT_FROM_NAME="${APP_NAME} Support"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET=
+R2_PRIVATE_BUCKET=
+R2_ENDPOINT=
+R2_URL=
+
+VITE_APP_NAME="${APP_NAME}"
+
+FRONTEND_URL=https://trustlab.dyzulk.com
+
+BROADCAST_CONNECTION=reverb
+
+REVERB_APP_ID=
+REVERB_APP_KEY=
+REVERB_APP_SECRET=
+REVERB_HOST="trustlab-api.dyzulk.com"
+REVERB_PORT=443
+REVERB_SCHEME=https
+
+VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
+VITE_REVERB_HOST="${REVERB_HOST}"
+VITE_REVERB_PORT="${REVERB_PORT}"
+VITE_REVERB_SCHEME="${REVERB_SCHEME}"
+
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+GITHUB_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+GITHUB_DEV_CLIENT_ID=
+GITHUB_DEV_CLIENT_SECRET=
+GITHUB_DEV_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+GITHUB_PROD_CLIENT_ID=
+GITHUB_PROD_CLIENT_SECRET=
+GITHUB_PROD_REDIRECT_URI=${APP_URL}/api/auth/github/callback
+
+
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+GOOGLE_REDIRECT_URI=${APP_URL}/api/auth/google/callback
+
+TELEGRAM_BOT_TOKEN=
+TELEGRAM_CHAT_ID=
diff --git a/.gitignore b/.gitignore
index 55d5875..a16319e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,6 @@
.DS_Store
.env
.env.backup
-.env.example.for.local
-.env.example.for.production
.env.production.editable
.env.production.soft.copy
.phpactor.json
@@ -28,3 +26,4 @@ Thumbs.db
*.sql
*.sqlite
.env.testing
+scripts/deploy-webhook.local.sh
diff --git a/app/Console/Commands/MigrateCaCertificates.php b/app/Console/Commands/MigrateCaCertificates.php
new file mode 100644
index 0000000..4c2fb6e
--- /dev/null
+++ b/app/Console/Commands/MigrateCaCertificates.php
@@ -0,0 +1,80 @@
+info('Starting CA data migration...');
+
+ // Check if source table exists
+ if (!DB::connection('mysql')->getSchemaBuilder()->hasTable('ca_certificates')) {
+ $this->error('Source table "ca_certificates" does not exist in the default connection.');
+ return 1;
+ }
+
+ // Check if target table is empty
+ $count = CaCertificate::count();
+ if ($count > 0 && !$this->option('force')) {
+ $this->error("Target table is not empty (contains $count records). Use --force to proceed.");
+ return 1;
+ }
+
+ // Fetch from old DB
+ $oldCerts = DB::connection('mysql')->table('ca_certificates')->get();
+
+ $this->info("Found {$oldCerts->count()} certificates to migrate.");
+
+ $bar = $this->output->createProgressBar($oldCerts->count());
+ $bar->start();
+
+ foreach ($oldCerts as $cert) {
+ // We use the Model to insert into the new DB (since it's now bound to 'mysql_ca')
+ // Using replicate() or manual array creation
+
+ $data = (array) $cert;
+
+ // Ensure we don't duplicate if it already exists (upsert-like behavior or strict check)
+ if (CaCertificate::where('uuid', $data['uuid'])->exists()) {
+ if ($this->option('force')) {
+ // Update existing
+ CaCertificate::where('uuid', $data['uuid'])->update($data);
+ } else {
+ // Skip
+ }
+ } else {
+ CaCertificate::create($data);
+ }
+
+ $bar->advance();
+ }
+
+ $bar->finish();
+ $this->newLine();
+ $this->info('Data migration completed successfully.');
+
+ return 0;
+ }
+}
diff --git a/app/Models/CaCertificate.php b/app/Models/CaCertificate.php
index 8c69c01..7690c0b 100644
--- a/app/Models/CaCertificate.php
+++ b/app/Models/CaCertificate.php
@@ -11,6 +11,8 @@ class CaCertificate extends Model
public $incrementing = false;
protected $keyType = 'string';
+ protected $connection = 'mysql_ca';
+
protected $fillable = [
'uuid',
'ca_type',
diff --git a/config/database.php b/config/database.php
index c57fa63..64f3882 100644
--- a/config/database.php
+++ b/config/database.php
@@ -63,6 +63,26 @@ return [
]) : [],
],
+ 'mysql_ca' => [
+ 'driver' => 'mysql',
+ 'url' => env('DB_CA_URL'),
+ 'host' => env('DB_CA_HOST', '127.0.0.1'),
+ 'port' => env('DB_CA_PORT', '3306'),
+ 'database' => env('DB_CA_DATABASE', 'trustlab_ca'),
+ 'username' => env('DB_CA_USERNAME', 'root'),
+ 'password' => env('DB_CA_PASSWORD', ''),
+ 'unix_socket' => env('DB_CA_SOCKET', ''),
+ 'charset' => env('DB_CA_CHARSET', 'utf8mb4'),
+ 'collation' => env('DB_CA_COLLATION', 'utf8mb4_unicode_ci'),
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'strict' => true,
+ 'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
+ ],
+
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
diff --git a/database/migrations/2026_01_06_000001_create_ca_certificates_table_new_db.php b/database/migrations/2026_01_06_000001_create_ca_certificates_table_new_db.php
new file mode 100644
index 0000000..700c13b
--- /dev/null
+++ b/database/migrations/2026_01_06_000001_create_ca_certificates_table_new_db.php
@@ -0,0 +1,40 @@
+create('ca_certificates', function (Blueprint $table) {
+ $table->string('uuid', 32)->primary();
+ $table->string('ca_type'); // root, intermediate_4096, intermediate_2048
+ $table->longText('cert_content')->nullable();
+ $table->longText('key_content')->nullable();
+ $table->string('serial_number')->nullable();
+ $table->string('common_name')->nullable();
+ $table->string('organization')->nullable();
+ $table->dateTime('valid_from')->nullable();
+ $table->dateTime('valid_to')->nullable();
+
+ // Tracking
+ $table->unsignedBigInteger('download_count')->default(0);
+ $table->timestamp('last_downloaded_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::connection('mysql_ca')->dropIfExists('ca_certificates');
+ }
+};
diff --git a/scripts/deploy-webhook.example.sh b/scripts/deploy-webhook.example.sh
new file mode 100644
index 0000000..cb23996
--- /dev/null
+++ b/scripts/deploy-webhook.example.sh
@@ -0,0 +1,121 @@
+#!/bin/bash
+
+# =========================================================================
+# TRUSTLAB DEPLOYMENT SCRIPT (EXAMPLE)
+# =========================================================================
+# CATATAN PENTING:
+# Script ini adalah CONTOH/TEMPLATE untuk digunakan di aaPanel Webhook.
+# Jangan jalankan script ini langsung dari repository jika belum dikonfigurasi.
+#
+# CARA PAKAI DI AAPANEL:
+# 1. Buka App Store > Webhook (atau Git Manager di versi baru).
+# 2. Add Webhook > Script.
+# 3. Copy-paste isi file ini ke dalam kolom Script di aaPanel.
+# 4. SESUAIKAN variable di bawah ini dengan konfigurasi server Anda.
+# =========================================================================
+
+# --- 1. KONFIGURASI SERVER (WAJIB DIEDIT DI AAPANEL) ---
+# Ganti dengan path project Anda yang sebenarnya
+PROJECT_PATH="/www/wwwroot/your-project.com"
+
+# Ganti dengan path PHP binary Anda (sesuai versi php)
+PHP_BIN="/www/server/php/83/bin/php"
+
+# =========================================================================
+# CONFIGURATION & ENVIRONMENT (JANGAN UBAH DI BAWAH INI KECUALI PAHAM)
+# =========================================================================
+export HOME=/root
+export COMPOSER_HOME=/root/.composer
+export PATH=$PATH:/usr/local/bin:/usr/bin:/bin
+
+# --- CONFIG TELEGRAM ---
+# Load from .env locally on server if available
+if [ -f .env ]; then
+ export $(grep -v '^#' .env | xargs)
+fi
+
+# Pastikan TELEGRAM_BOT_TOKEN dan TELEGRAM_CHAT_ID ada di .env server Anda
+BOT_TOKEN="${TELEGRAM_BOT_TOKEN}"
+CHAT_ID="${TELEGRAM_CHAT_ID}"
+
+send_telegram() {
+ local message="$1"
+ if [ -n "$BOT_TOKEN" ] && [ -n "$CHAT_ID" ]; then
+ curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
+ -d chat_id="$CHAT_ID" \
+ -d text="$message" \
+ -d parse_mode="HTML" > /dev/null
+ else
+ echo "โ ๏ธ Telegram credentials missing, skipping notification."
+ fi
+}
+
+# =========================================================================
+# START DEPLOYMENT
+# =========================================================================
+echo "๐ Starting Deployment..."
+send_telegram "โณ Deployment Started%0A%0A๐ Project: TrustLab API%0A๐
Date: $(date)"
+
+set -e
+
+# Safety check directory
+if [ ! -d "$PROJECT_PATH" ]; then
+ echo "โ Error: Project path $PROJECT_PATH does not exist."
+ exit 1
+fi
+
+git config --global --add safe.directory "$PROJECT_PATH"
+cd "$PROJECT_PATH"
+
+trap 'send_telegram "โ Deployment FAILED!%0A%0Aโ ๏ธ Check server logs untuk detail.%0A๐
Date: $(date)"; exit 1' ERR
+
+# 3. Pull & Clean
+echo "๐ฅ Pulling latest code..."
+git pull origin main
+
+echo "๐งน Cleaning untracked files..."
+git clean -fd
+
+# 4. PHP Dependencies
+echo "๐ฆ Updating Composer dependencies..."
+$PHP_BIN /usr/bin/composer install --no-dev --optimize-autoloader --no-interaction
+
+# 5. Frontend Assets
+echo "๐ฆ Building frontend assets..."
+npm install
+
+echo "๐ง Fixing permissions..."
+find node_modules -type f \( -path "*/bin/*" -o -path "*/.bin/*" \) -exec chmod +x {} \;
+if [ -d "node_modules/@esbuild/linux-x64/bin" ]; then
+ chmod +x node_modules/@esbuild/linux-x64/bin/esbuild
+fi
+
+rm -rf public/build
+echo "๐ Running Vite build..."
+npx vite build
+
+echo "๐งน Pruning dev dependencies..."
+npm prune --omit=dev
+
+# 6. Environment Setup
+if [ -f .env.production.editable ]; then
+ echo "๐ Updating .env from .env.production.editable..."
+ cp .env.production.editable .env
+elif [ ! -f .env ]; then
+ cp .env.production.example .env
+fi
+
+# 7. Laravel Optimizations
+echo "โก Optimizing Laravel..."
+$PHP_BIN artisan optimize:clear
+$PHP_BIN artisan migrate --force
+
+# NEW: Conditional CA Data Migration
+# $PHP_BIN artisan ca:migrate-data
+
+$PHP_BIN artisan config:cache
+$PHP_BIN artisan route:cache
+$PHP_BIN artisan view:cache
+
+echo "โ
Deployment SUCCESS!"
+send_telegram "โ
Deployment Success!%0A%0A๐ฆ Project: TrustLab API%0A๐
Date: $(date)"