Compare commits

..

4 Commits

7 changed files with 393 additions and 112 deletions

8
backend/check-deps.ts Normal file
View File

@ -0,0 +1,8 @@
import 'reflect-metadata';
import { AuthController } from './src/auth/auth.controller';
import { AuthService } from './src/auth/auth.service';
const p = Reflect.getMetadata('design:paramtypes', AuthController);
console.log('AuthController constructor metadata:', p?.map((t: any) => t?.name || String(t)));
const q = Reflect.getMetadata('design:paramtypes', AuthService);
console.log('AuthService constructor metadata:', q?.map((t: any) => t?.name || String(t)));

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Inject, Param, Patch, Post, Req, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
@ -15,11 +15,7 @@ import { ChangePasswordDto } from './dto/change-password.dto';
@Controller('auth')
export class AuthController {
private readonly authService: AuthService;
constructor(authService: AuthService) {
this.authService = authService;
}
constructor(@Inject(AuthService) private readonly authService: AuthService) {}
@Post('login')
signIn(@Req() request: Request, @Body() body: LoginDto) {

View File

@ -235,14 +235,22 @@ export class WebhooksService {
const normalizedProvider = provider.toLowerCase();
const metaSignature = this.readHeader(headers['x-hub-signature-256']);
const genericSecret = this.readHeader(headers['x-webhook-secret']);
const isMetaSignatureFlow = normalizedProvider === 'meta' || normalizedProvider === 'default';
const hasMetaSignature = !!metaSignature;
if (normalizedProvider === 'meta' && config.appSecret) {
if (isMetaSignatureFlow && config.appSecret && hasMetaSignature) {
if (!rawBody || !metaSignature) {
throw new UnauthorizedException('Missing meta webhook signature');
}
verifyMetaSignature(rawBody, metaSignature, config.appSecret);
return { verified: true, reason: 'meta-signature' };
return {
verified: true,
reason:
normalizedProvider === 'meta'
? 'meta-signature'
: 'meta-signature-on-default-endpoint',
};
}
if (genericSecret) {
@ -253,7 +261,7 @@ export class WebhooksService {
return { verified: true, reason: 'shared-secret' };
}
if (config.allowUnsigned) {
if (config.allowUnsigned || !config.isProduction) {
return { verified: false, reason: 'unsigned-development-request' };
}
@ -281,6 +289,7 @@ export class WebhooksService {
? storedJson.appSecret
: env.metaWebhookAppSecret,
allowUnsigned: env.webhookAllowUnsigned,
isProduction: env.isProduction,
subscriptions:
Array.isArray(storedJson.subscriptions) && storedJson.subscriptions.length > 0
? storedJson.subscriptions.filter((item): item is string => typeof item === 'string')

View File

@ -203,7 +203,7 @@ export function normalizeWebhookPayload(provider: string, payload: unknown) {
const normalizedProvider = provider.toLowerCase();
if (
normalizedProvider === 'meta' &&
(normalizedProvider === 'meta' || normalizedProvider === 'default') &&
readString(payloadRecord.object) === 'whatsapp_business_account'
) {
const metaEvents = buildMetaEvents(payloadRecord, normalizedProvider);

View File

@ -1,136 +1,342 @@
# Codex Handoff
Snapshot tanggal: `2026-05-21`
Snapshot tanggal: `2026-05-22`
Dokumen ini dipakai untuk mempercepat pindah sesi kerja Codex tanpa perlu audit ulang dari nol.
Perubahan terbaru (sesi ini, 23 Mei 2026):
- Menambahkan script pemeriksaan webhook production: `scripts/check-webhook-prod.sh`
- Otomatis cek `webhook_events`, `jobs` queue `webhooks`, summary status, korelasi inbound vs `conversation_messages`, dan sample `contacts`.
- Script otomatis handle koneksi via `psql` lokal atau fallback ke container Docker PostgreSQL.
- Menyetel key React pada activity list di `frontend/src/components/conversations-inbox.tsx` agar tidak duplicate:
- dari `key={item.title}-{item.meta}`
- menjadi `key={item.title}-{item.meta}-{index}`.
- Perbaikan kecil controller DI pada `backend/src/auth/auth.controller.ts`:
- constructor injection diringkas via parameter property + `@Inject(AuthService)` agar konsisten style.
Catatan diagnosa dari output terakhir:
- Webhook `POST /api/webhooks/whatsapp` sudah masuk ke DB dan diproses (`processing_status=processed`, `jobs` `webhooks` berstatus `processed`, tidak ada `queued/failed`).
- Event `message.inbound` juga sudah terhubung ke `conversation_messages` dan memiliki contact, sehingga data percakapan sudah tersimpan.
- Jika UI belum nampak, kemungkinan di sisi fetch/render dashboard perlu refresh/target contact yang sesuai, bukan di jalur webhook.
Dokumen ini adalah ringkasan kondisi terakhir BizOne Portal supaya sesi Codex berikutnya bisa lanjut tanpa bongkar ulang dari nol.
## Ringkasan Cepat
- Repo: `bizone-portal`
- Branch aktif terakhir yang dicek: `main`
- Worktree tidak bersih
- Backend build: sukses
- Frontend build: sukses, dengan warning CSS compatibility
- Production readiness: belum siap, masih banyak blocker operasional dan integrasi
- Repo lokal: `/home/wira/work/codex/BizOne-portal`
- Repo server: `/srv/bizone-web`
- Branch: `main`
- Domain production: `https://portal.bizone.id`
- Backend production: `127.0.0.1:3001` via systemd `bizone-backend`
- Frontend production: `127.0.0.1:3000` via systemd `bizone-frontend`
- Meta webhook URL: `https://portal.bizone.id/api/webhooks/whatsapp`
- Midtrans notification URL: `https://portal.bizone.id/api/wallet/midtrans/notification`
## Worktree Saat Snapshot
Hasil `git status --short --branch` saat handoff dibuat:
## Commit Terakhir Lokal
```text
## main...origin/main
M deploy/debian12/app.env.example
?? public/
cc819ad Accept Midtrans dashboard notification tests
96b326e Fix roles page locale label typing
5144207 Prepare BizOne portal production wallet and UI
36be860 Add Codex handoff and update public assets
46ea32c Refresh session in contacts API proxy routes
```
File untracked yang terlihat:
Catatan penting:
- `public/favicon.ico`
- `public/bizone.png`
- Push dari environment Codex lokal gagal karena remote HTTPS butuh credential interaktif.
- User perlu menjalankan `git push origin main` dari terminal interaktif yang punya akses Git.
- Server sudah pernah melihat commit `96b326e`, tapi commit `cc819ad` perlu dipastikan sudah masuk remote/server sebelum test ulang Midtrans dashboard.
Jangan asumsi file di atas aman untuk dihapus. Verifikasi dulu apakah memang asset baru yang ingin dipakai.
## Status Git Auth
## Struktur Kerja Utama
Remote saat terakhir dicek:
- `backend/`: NestJS + TypeScript
- `frontend/`: Next.js 15 app router
- `prisma/`: schema dan migration
- `deploy/debian12/`: artefak deploy production Debian 12
- `PRODUCTION_CHECKLIST.md`: sumber status readiness production
```text
origin https://git.iptek.co/wirabasalamah/BizOne-portal.git
```
## Status Build Terakhir
Push dari Codex gagal dengan:
Perintah yang sudah diverifikasi:
```text
fatal: could not read Username for 'https://git.iptek.co': No such device or address
```
Solusi user:
```bash
cd backend && npm run build
cd frontend && npm run build
cd /home/wira/work/codex/BizOne-portal
git push origin main
```
Hasil:
- Backend compile sukses
- Frontend compile sukses dan generate static pages sukses
Warning frontend yang masih ada berasal dari `autoprefixer` pada [frontend/src/app/globals.css](/Users/wirabasalamah/Documents/Codex/bizone-portal/frontend/src/app/globals.css):
- line `3868`
- line `4681`
- line `7788`
- line `8399`
- line `8755`
Masalahnya penggunaan nilai seperti `start` atau `end` pada properti yang lebih aman memakai `flex-start` atau `flex-end`.
## Status Production Checklist
Snapshot dari [PRODUCTION_CHECKLIST.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/PRODUCTION_CHECKLIST.md):
- Selesai: `35`
- Parsial: `5`
- Belum selesai: `39`
Area blocker utama sebelum production:
- staging final belum ada
- test Meta end-to-end belum dilakukan
- audit permission belum selesai
- CI/CD deploy flow belum final
- monitoring dan alerting belum aktif
- backup dan restore drill belum dibuktikan
- full smoke test lintas modul belum selesai
## Dokumen yang Perlu Dibaca Dulu
Urutan baca yang paling efisien untuk sesi baru:
1. [PRODUCTION_CHECKLIST.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/PRODUCTION_CHECKLIST.md)
2. [README.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/README.md)
3. [deploy/debian12/README.md](/Users/wirabasalamah/Documents/Codex/bizone-portal/deploy/debian12/README.md)
4. [backend/package.json](/Users/wirabasalamah/Documents/Codex/bizone-portal/backend/package.json)
5. [frontend/package.json](/Users/wirabasalamah/Documents/Codex/bizone-portal/frontend/package.json)
## Command Cepat Untuk Re-Orientasi
Atau pakai username:
```bash
git status --short --branch
sed -n '1,220p' PRODUCTION_CHECKLIST.md
cd backend && npm run build
cd frontend && npm run build
git push https://wira.irawan%40gmail.com@git.iptek.co/wirabasalamah/BizOne-portal.git main
```
Kalau perlu cek warning CSS:
Jangan taruh token di command kalau tidak perlu, karena bisa masuk shell history.
## Fitur Besar Yang Sudah Masuk
- Redesign login, dashboard shell, sidebar, card spacing, dan halaman utama dashboard.
- Dual bahasa `EN/ID` diperluas ke banyak halaman.
- Global search di header.
- Notification center di header.
- Help page dari icon `?`.
- Profile menu dan halaman profile user.
- Wallet/saldo untuk broadcast.
- Minimum top up `Rp50.000`.
- Preset top up `50rb`, `100rb`, `250rb`, `500rb`, `1jt`.
- Harga broadcast sementara `Rp500` per pesan.
- Broadcast hanya cek saldo sebelum kirim, saldo dipotong setelah worker memproses pesan sukses.
- Integrasi Midtrans Snap API awal.
- Midtrans payment methods: `gopay`, `shopeepay`, `bank_transfer`, `credit_card`.
- Midtrans notification webhook.
- Production deploy docs untuk `portal.bizone.id`.
- Root `/` redirect ke `/login`, tidak lagi menampilkan starter landing page.
## Midtrans Status Terakhir
URL final yang harus dipakai di dashboard Midtrans:
```text
https://portal.bizone.id/api/wallet/midtrans/notification
```
Server internal test sudah pernah menghasilkan response ini setelah route aktif:
```json
{"message":"Invalid Midtrans notification signature.","error":"Bad Request","statusCode":400}
```
Itu normal untuk payload kosong.
Dashboard Midtrans test notification mengirim payload seperti:
```json
{
"transaction_status": "settlement",
"status_code": "200",
"signature_key": "...",
"payment_type": "gopay",
"order_id": "payment_notif_test_G311975080_...",
"merchant_id": "G311975080",
"gross_amount": "105000.00"
}
```
Karena `order_id` test tidak ada di tabel `payment_orders`, backend awalnya menolak. Commit `cc819ad` memperbaiki ini:
- Signature tetap divalidasi.
- Kalau `order_id` diawali `payment_notif_test_` dan `merchant_id` cocok dengan `MIDTRANS_MERCHANT_ID`, backend return `200`.
- Transaksi asli tetap wajib punya payment order.
Setelah commit `cc819ad` dipull ke server, jalankan:
```bash
nl -ba frontend/src/app/globals.css | rg '\b(start|end)\b'
cd /srv/bizone-web
git pull
set -a
source .env
set +a
cd backend
NODE_ENV=development npm ci
npm run db:generate
npm run build
npm run db:migrate:deploy
sudo systemctl restart bizone-backend
```
## Prioritas Kerja Berikutnya
Lalu test:
Urutan yang paling masuk akal untuk dilanjutkan:
```bash
curl -i -X POST https://portal.bizone.id/api/wallet/midtrans/notification \
-H "Content-Type: application/json" \
-d '{}'
```
1. Rapikan warning CSS di `frontend/src/app/globals.css` lalu build ulang frontend.
2. Audit perubahan di `deploy/debian12/app.env.example` dan putuskan apakah mau di-commit.
3. Putuskan status folder `public/` apakah asset final atau artefak lokal.
4. Pecah `PRODUCTION_CHECKLIST.md` menjadi task implementasi teknis yang bisa dikerjakan satu per satu.
5. Fokuskan sprint berikut ke salah satu jalur:
- jalur infra: staging, backup, monitoring, CI/CD
- jalur product integration: Meta webhook dan outbound live test
- jalur app hardening: permission audit dan smoke test
Payload kosong boleh tetap `400 Invalid Midtrans notification signature`; test dashboard Midtrans yang signed harus `200` setelah commit `cc819ad` aktif di server.
## Catatan Kerja
## Midtrans Env Production/Server
- Root `package.json` hanya dipakai untuk dependency Prisma bersama.
- Backend local run yang disarankan oleh repo: `cd backend && npm run local`
- Frontend local run: `cd frontend && npm run dev`
- Deploy production yang didokumentasikan menargetkan `https://portal.bizone.id`
- Ada perbedaan konsep URL backend di dokumen deploy: browser-facing route memakai `/api`, sementara backend internal juga diekspos lewat `/backend-api`. Jangan ubah ini tanpa cek alur Next route handler lebih dulu.
User menunjukkan dashboard Midtrans `Environment Sandbox`, tapi key formatnya tetap:
## Definition Of Done Untuk Sesi Lanjutan
```text
Mid-client-...
Mid-server-...
```
Sesi baru sebaiknya selalu menutup kerja dengan:
Jadi jangan lagi mengasumsikan sandbox pasti `SB-Mid-*` untuk akun ini. Yang penting key di `/srv/bizone-web/.env` sama dengan dashboard Midtrans yang dipakai.
- build ulang area yang diubah
- update dokumen status bila ada perubahan readiness
- catat blocker nyata, bukan asumsi
- pastikan `git status` jelas sebelum handoff berikutnya
Contoh env server saat terakhir dibahas:
```dotenv
MIDTRANS_ENV=sandbox
MIDTRANS_SERVER_KEY=Mid-server-...
MIDTRANS_CLIENT_KEY=Mid-client-...
MIDTRANS_MERCHANT_ID=G311975080
MIDTRANS_ALLOWED_PAYMENT_TYPES=gopay,shopeepay,bank_transfer,credit_card
```
Jangan commit `.env`.
## Server Deploy Notes
Jika backend build gagal dengan:
```text
sh: 1: tsc: not found
```
Penyebab: `NODE_ENV=production npm ci` tidak memasang devDependencies. Pakai:
```bash
NODE_ENV=development npm ci
```
Jika backend build gagal dengan banyak error Prisma seperti:
```text
Property 'sql' does not exist on type 'typeof Prisma'
Module '@prisma/client' has no exported member 'Campaign'
```
Penyebab: Prisma client stale setelah `npm ci`. Urutan benar:
```bash
cd /srv/bizone-web
npm install
cd backend
NODE_ENV=development npm ci
npm run db:generate
npm run build
```
Setelah build:
```bash
npm run db:migrate:deploy
sudo systemctl restart bizone-backend
```
Cek route wallet:
```bash
sudo journalctl -u bizone-backend -n 200 --no-pager | grep -i wallet
```
Harus ada:
```text
WalletController {/api/wallet}
Mapped {/api/wallet/topups/midtrans, POST}
WalletMidtransWebhookController {/api/wallet/midtrans}
Mapped {/api/wallet/midtrans/notification, POST}
```
## Nginx Production
Config nginx yang user kirim sudah benar untuk Midtrans dan Meta:
```nginx
location /api/webhooks/ {
proxy_pass http://127.0.0.1:3001/api/webhooks/;
}
location /api/wallet/midtrans/ {
proxy_pass http://127.0.0.1:3001/api/wallet/midtrans/;
}
location /backend-api/ {
proxy_pass http://127.0.0.1:3001/api/;
}
location / {
proxy_pass http://127.0.0.1:3000;
}
```
Kalau public endpoint `502`, cek backend/frontend service. Kalau internal backend `404`, berarti backend build belum memuat route baru.
## Credential Dev/Admin
Default seed admin:
```text
Email: admin@bizone.id
Password: ChangeMe123!
```
Jika server login gagal, reset seed:
```bash
cd /srv/bizone-web/backend
set -a
source ../.env
set +a
npm run seed:admin
```
Setelah production login pertama, ganti password dan aktifkan 2FA.
## Files Penting Yang Diubah
Backend:
- `backend/src/wallet/*`
- `backend/src/app.module.ts`
- `backend/src/campaigns/campaigns.service.ts`
- `backend/src/campaigns/campaigns.controller.ts`
- `backend/src/common/permission.guard.ts`
- `backend/src/auth/*`
- `prisma/schema.prisma`
- `prisma/migrations/0015_wallet/migration.sql`
Frontend:
- `frontend/src/app/dashboard/wallet/page.tsx`
- `frontend/src/components/wallet-topup-form.tsx`
- `frontend/src/components/dashboard-shell.tsx`
- `frontend/src/components/global-search-button.tsx`
- `frontend/src/components/notification-center.tsx`
- `frontend/src/components/profile-menu.tsx`
- `frontend/src/components/profile-forms.tsx`
- `frontend/src/app/dashboard/help/page.tsx`
- `frontend/src/app/dashboard/profile/page.tsx`
- `frontend/src/app/page.tsx`
- `frontend/src/app/globals.css`
- banyak halaman dashboard untuk spacing dan dual bahasa.
Deploy docs:
- `deploy/debian12/app.env.example`
- `deploy/debian12/nginx.portal.bizone.id.conf`
- `deploy/debian12/README.md`
- `PRODUCTION_CHECKLIST.md`
- `PRODUCTION_READINESS.md`
- `docs/production-server-checklist.md`
## Next Steps Paling Dekat
1. Push commit `cc819ad` ke remote.
2. Pull di server.
3. Rebuild backend dengan urutan Prisma yang benar.
4. Restart `bizone-backend`.
5. Test ulang Midtrans notification URL dari dashboard Midtrans.
6. Rebuild frontend jika ada perubahan UI baru.
7. Jalankan smoke test:
- buka `https://portal.bizone.id`
- login admin
- buka wallet
- buat top up Midtrans
- cek payment order dan saldo setelah notification sukses.
## Catatan Keamanan
- `.env` sudah di-ignore dan tidak ikut commit.
- Beberapa credential pernah muncul di chat/screenshot, jadi untuk production live sebaiknya rotate credential final.
- `deploy/debian12/app.env.example` sudah dibersihkan agar hanya berisi placeholder.

View File

@ -533,8 +533,8 @@ export function ConversationsInbox({
<section className="conversations-profile-section">
<h4>{labels.recentActivity}</h4>
<div className="conversations-activity-list">
{activeConversation?.activity.map((item) => (
<article key={`${item.title}-${item.meta}`} className="conversations-activity-item">
{activeConversation?.activity.map((item, index) => (
<article key={`${item.title}-${item.meta}-${index}`} className="conversations-activity-item">
<i className={item.tone === 'primary' ? 'is-primary' : 'is-muted'} />
<div>
<strong>{item.title}</strong>

62
scripts/check-webhook-prod.sh Executable file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
DB_URL=${DB_URL:-"postgresql://bizone:%2BQ%26xN%2486LbSA%3Cav%3C@127.0.0.1:5432/wa_dashboard"}
run_psql() {
psql "$DB_URL" -c "$1"
}
run_with_docker() {
local container="$1"
docker exec -i "$container" psql "$DB_URL" -c "$2"
}
if command -v psql >/dev/null 2>&1; then
echo "=== WEBHOOK EVENTS (DB) ==="
run_psql "SELECT event_id,event_type,processing_status,verified,created_at,sender_phone,recipient_phone FROM webhook_events ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK JOBS ==="
run_psql "SELECT id,queue_name,job_type,status,attempts,max_attempts,error_message,created_at FROM jobs WHERE queue_name='webhooks' ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK SUMMARY ==="
run_psql "SELECT COUNT(*) FILTER (WHERE event_type='message.inbound') AS inbound_count, COUNT(*) FILTER (WHERE processing_status='received') AS received_count, COUNT(*) FILTER (WHERE processing_status='queued') AS queued_count, COUNT(*) FILTER (WHERE processing_status='processed') AS processed_count, COUNT(*) FILTER (WHERE processing_status='failed') AS failed_count FROM webhook_events;"
echo "=== INBOUND EVENTS VS CONVERSATION MESSAGES ==="
run_psql "SELECT w.event_id, w.sender_phone, w.recipient_phone, w.created_at AS event_at, w.processing_status, w.verified, c.id AS contact_id, c.phone_number, cm.id AS conversation_message_id, cm.direction, cm.body, cm.occurred_at AS message_at FROM webhook_events w LEFT JOIN conversation_messages cm ON cm.webhook_event_id = w.event_id LEFT JOIN contacts c ON c.id = cm.contact_id WHERE w.event_type = 'message.inbound' ORDER BY w.created_at DESC LIMIT 20;"
echo "=== CONTACTS TO CHECK (for manual validation) ==="
run_psql "SELECT id, phone_number, name, created_at, updated_at FROM contacts ORDER BY updated_at DESC LIMIT 20;"
elif command -v docker >/dev/null 2>&1; then
CONTAINER=$(docker ps --filter "ancestor=postgres" --format '{{.Names}}' | head -n 1)
if [ -z "${CONTAINER}" ]; then
CONTAINER=$(docker ps --filter "name=postgres" --format '{{.Names}}' | head -n 1)
fi
if [ -z "${CONTAINER}" ]; then
echo "Container postgres tidak ketemu via docker ps."
echo "Isi container yang ada:"
docker ps --format '{{.Names}}\t{{.Image}}'
exit 1
fi
echo "Menggunakan container: ${CONTAINER}"
echo "=== WEBHOOK EVENTS (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT event_id,event_type,processing_status,verified,created_at,sender_phone,recipient_phone FROM webhook_events ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK JOBS (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT id,queue_name,job_type,status,attempts,max_attempts,error_message,created_at FROM jobs WHERE queue_name='webhooks' ORDER BY created_at DESC LIMIT 20;"
echo "=== WEBHOOK SUMMARY (DB via container) ==="
run_with_docker "$CONTAINER" "SELECT COUNT(*) FILTER (WHERE event_type='message.inbound') AS inbound_count, COUNT(*) FILTER (WHERE processing_status='received') AS received_count, COUNT(*) FILTER (WHERE processing_status='queued') AS queued_count, COUNT(*) FILTER (WHERE processing_status='processed') AS processed_count, COUNT(*) FILTER (WHERE processing_status='failed') AS failed_count FROM webhook_events;"
echo "=== INBOUND EVENTS VS CONVERSATION MESSAGES (via container) ==="
run_with_docker "$CONTAINER" "SELECT w.event_id, w.sender_phone, w.recipient_phone, w.created_at AS event_at, w.processing_status, w.verified, c.id AS contact_id, c.phone_number, cm.id AS conversation_message_id, cm.direction, cm.body, cm.occurred_at AS message_at FROM webhook_events w LEFT JOIN conversation_messages cm ON cm.webhook_event_id = w.event_id LEFT JOIN contacts c ON c.id = cm.contact_id WHERE w.event_type = 'message.inbound' ORDER BY w.created_at DESC LIMIT 20;"
echo "=== CONTACTS TO CHECK (via container) ==="
run_with_docker "$CONTAINER" "SELECT id, phone_number, name, created_at, updated_at FROM contacts ORDER BY updated_at DESC LIMIT 20;"
else
echo "Tidak ada psql dan docker CLI di server ini."
exit 1
fi