Compare commits

...

45 Commits

Author SHA1 Message Date
04c2e0d580 chore(release): 1.6.6 2026-03-03 16:41:45 +08:00
bipproduction
6dba07baac fix: prisma connection exhaustion & firebase lazy init
- prisma/schema.prisma: tambah binaryTargets debian & linux-musl untuk Docker
- src/lib/prisma.ts: pakai global singleton di dev & prod, hapus eager $connect()
- src/lib/firebase-admin.ts: lazy initialization agar tidak crash saat build time
- .env.example: lengkap dengan semua env variable + connection_limit & pool_timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:26:48 +08:00
b76c7a4b1c Merge pull request 'bug-prisma 3' (#65) from bug-prisma/3-mar-26 into staging
Reviewed-on: #65
2026-03-03 15:36:43 +08:00
240f6eb7c2 Fix console 2026-03-03 15:34:36 +08:00
f64ae42825 Fix Prisma
1 fix: error koneksi Prisma dengan retry mechanism
      2
      3 Perubahan:
      4 - src/lib/prisma.ts: Tambah retry (3x) dengan exponential backoff saat connect
      5 - src/lib/prisma-retry.ts: NEW - Utility wrapper untuk retry operations
      6 - src/app/api/user-validate/route.ts: Improve error logging dengan detail
      7 - src/middleware.tsx: Clean up commented code
      8
      9 Fitur:
     10 - Auto retry saat database connection gagal
     11 - Explicit () di production
     12 - Better error logging untuk debugging
     13 - Reusable retry wrapper (withRetry, withTimeout)
     14
     15 Testing:
     16 - Build berhasil 
     17 - Type checking passed 
     18
     19 Fixes: Error in PostgreSQL connection: Error { kind: Closed, cause: None }

### No Issue
2026-03-03 15:30:34 +08:00
df5d1aad48 chore(release): 1.6.5 2026-03-03 15:25:39 +08:00
82e69309a1 Merge pull request 'bug-prisma 2' (#64) from bug-prisma/3-mar-26 into staging
Reviewed-on: #64
2026-03-03 14:19:10 +08:00
a6588818b5 fix: error koneksi Prisma - DATABASE_URL tidak loaded di
production
- Tambah validasi DATABASE_URL di prisma.ts
- Tambah copy .env file di postbuild script

### No Issue
2026-03-03 14:15:15 +08:00
58e1afaa45 chore(release): 1.6.4 2026-03-03 14:14:00 +08:00
f65f9b7834 Merge pull request 'bug-prisma' (#63) from bug-prisma/3-mar-26 into staging
Reviewed-on: #63
2026-03-03 12:11:04 +08:00
250b7c5261 Fix Prisma 2026-03-03 12:03:30 +08:00
935e519662 chore(release): 1.6.3 2026-03-03 11:55:25 +08:00
36b9248ed7 Merge pull request 'fix-bug middle' (#62) from fix-bug/25-feb-26 into staging
Reviewed-on: #62
2026-02-25 15:35:20 +08:00
c6dbd152d5 Fix middleware
### NO Issue
2026-02-25 15:34:22 +08:00
714cf5cd5a chore(release): 1.6.2 2026-02-25 15:22:40 +08:00
a2c5f053da Merge pull request 'fix-bug/25-feb-26' (#61) from fix-bug/25-feb-26 into staging
Reviewed-on: #61
2026-02-25 14:34:14 +08:00
a68343599d Fix type env
Fix:
- modified:   types/env.d.ts

### No Issue
2026-02-25 14:25:13 +08:00
419b87fc92 chore(release): 1.6.1 2026-02-25 12:00:20 +08:00
bedc0cc88b Merge pull request 'Delete termsOfServiceAccepted on register' (#60) from mobile-api/24-feb-26 into staging
Reviewed-on: #60
2026-02-25 10:46:17 +08:00
0271c87ba9 Delete termsOfServiceAccepted on register
### No issue
2026-02-24 18:04:05 +08:00
a9fbd544e5 Merge pull request 'mobile-api/24-feb-26' (#59) from mobile-api/24-feb-26 into staging
Reviewed-on: #59
2026-02-24 07:42:44 +08:00
5551f30721 Fix API and clear code
modified:   src/app/api/auth/register/route.ts
 modified:   src/app_modules/auth/login/view.tsx

### No Issue
2026-02-24 07:38:44 +08:00
00d36454d1 chore(release): 1.6.0 2026-02-24 07:33:28 +08:00
a762fbe9b1 Fix Api Mobile
API – Admin Forum
- src/app/api/mobile/admin/forum/route.ts
- src/app/api/mobile/admin/forum/[id]/comment/route.ts
- src/app/api/mobile/admin/forum/[id]/report-posting/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-20 16:47:28 +08:00
9afd741d4f Merge pull request 'Fix Admin API Mobile' (#58) from mobile-api/18-feb-26 into staging
Reviewed-on: #58
2026-02-18 17:33:11 +08:00
817919f8f7 Merge pull request '### Fitur: Penambahan Pagination pada Endpoint Admin Mobile' (#57) from mobile-api/14-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/57
2026-02-14 16:25:39 +08:00
90031e23ef Merge pull request 'Fix Api Mobile' (#56) from mobile-api/13-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/56
2026-02-13 17:41:31 +08:00
596ebd2ff4 Merge pull request 'mobile-api/12-feb-26' (#55) from mobile-api/12-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/55
2026-02-12 17:49:00 +08:00
aa700523ca Merge pull request 'feat: Implementasi pagination pada endpoint mobile donation' (#54) from mobile-api/10-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/54
2026-02-10 17:36:02 +08:00
236ab4d4a4 Merge pull request 'Mobile API' (#53) from mobile-api/9-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/53
2026-02-09 17:38:52 +08:00
eaa7692359 Merge pull request 'Fix API mobile Investment' (#52) from mobile-api/6-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/52
2026-02-06 17:39:36 +08:00
d51ce346e6 Merge pull request 'Fix API mobile' (#51) from mobile-api/5-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/51
2026-02-05 17:36:40 +08:00
91f4bb6c9e Merge pull request 'mobile-api/4-jan-26' (#50) from mobile-api/4-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/50
2026-02-05 10:11:03 +08:00
1fe0001994 Merge pull request 'Fix API Job untuk loaddata:' (#49) from mobile-api/2-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/49
2026-02-02 17:11:32 +08:00
b82a283731 Merge pull request 'mobile-api for load data' (#48) from mobile-api/30-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/48
2026-01-30 17:20:09 +08:00
6d7d0fd07e Merge pull request 'mobile-notification done' (#46) from mobile-notification/27-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/46
2026-01-27 17:00:26 +08:00
bc80bb3441 Merge pull request 'Notification Donasi & EULA on login' (#45) from mobile-notification/23-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/45
2026-01-23 17:06:56 +08:00
8ab94b9c86 Merge pull request 'mobile-notification invesment' (#44) from mobile-notification/21-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/44
2026-01-21 15:41:18 +08:00
6e37b18e42 Merge pull request 'mobile-notification report comment' (#43) from mobile-notification/19-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/43
2026-01-19 17:54:21 +08:00
a6db03d0b4 Merge pull request 'mobile-notification event dan voting' (#42) from mobile-notification/15-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/42
2026-01-15 17:41:58 +08:00
c550a4e922 Merge pull request 'mobile-notification try to push to apple and android preview' (#41) from mobile-notification/12-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/41
2026-01-15 17:41:13 +08:00
2431a3fa3e Merge pull request 'Mobile notification & EULA route' (#40) from mobile-notification/9-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/40
2026-01-09 17:47:40 +08:00
1ed0da8c7d Merge pull request 'Fix API mobile notifikasi untuk job' (#39) from mobile-notification/7-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/39
2026-01-08 15:26:19 +08:00
e15a5d796d Merge pull request 'mobile notification' (#38) from mobile-notification/6-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/38
2026-01-06 17:53:14 +08:00
836ebfaef0 Merge pull request 'mobile notification API' (#37) from mobile-notification/5-jan-25 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/37
2026-01-05 14:06:58 +08:00
19 changed files with 469 additions and 103 deletions

53
.env.example Normal file
View File

@@ -0,0 +1,53 @@
# ==============================
# Database
# ==============================
# Tambahkan connection_limit dan pool_timeout untuk mencegah connection exhaustion
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20"
# ==============================
# Auth / Session
# ==============================
WIBU_PWD="your_wibu_password"
NEXT_PUBLIC_BASE_TOKEN_KEY="your_token_key"
NEXT_PUBLIC_BASE_SESSION_KEY="your_session_key"
# ==============================
# Payment Gateway (Midtrans)
# ==============================
Client_KEY="your_midtrans_client_key"
Server_KEY="your_midtrans_server_key"
# ==============================
# Maps
# ==============================
MAPBOX_TOKEN="your_mapbox_token"
# ==============================
# Realtime (WebSocket)
# ==============================
WS_APIKEY="your_ws_api_key"
NEXT_PUBLIC_WIBU_REALTIME_TOKEN="your_realtime_token"
# ==============================
# Email (Resend)
# ==============================
RESEND_APIKEY="your_resend_api_key"
# ==============================
# WhatsApp
# ==============================
WA_SERVER_TOKEN="your_wa_server_token"
# ==============================
# Firebase Admin (Push Notification)
# ==============================
FIREBASE_ADMIN_PROJECT_ID="your_firebase_project_id"
FIREBASE_ADMIN_CLIENT_EMAIL="your_firebase_client_email@project.iam.gserviceaccount.com"
# Private key: salin dari service account JSON, ganti newline dengan \n
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"
# ==============================
# App
# ==============================
NEXT_PUBLIC_API_URL="http://localhost:3000"
LOG_LEVEL="info"

View File

@@ -2,6 +2,37 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03)
### Bug Fixes
* prisma connection exhaustion & firebase lazy init ([6dba07b](https://wibugit.wibudev.com/wibu/hipmi/commit/6dba07baac6a3ef7d264c13e378a905002401e1b))
## [1.6.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.4...v1.6.5) (2026-03-03)
### Bug Fixes
* error koneksi Prisma - DATABASE_URL tidak loaded di ([a658881](https://wibugit.wibudev.com/wibu/hipmi/commit/a6588818b5d8018b3a634e0ae0846e309569d370))
## [1.6.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.3...v1.6.4) (2026-03-03)
## [1.6.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.2...v1.6.3) (2026-03-03)
## [1.6.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.1...v1.6.2) (2026-02-25)
## [1.6.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.0...v1.6.1) (2026-02-25)
## [1.6.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.40...v1.6.0) (2026-02-23)
### Features
* Implementasi pagination pada endpoint mobile donation ([e89886e](https://wibugit.wibudev.com/wibu/hipmi/commit/e89886e1dbc8cb4d95e6cc7c2787fb22a1dcaf56))
* Tambahkan pagination pada API mobile investasi ([a7694bd](https://wibugit.wibudev.com/wibu/hipmi/commit/a7694bd7d5d72b6499443faf99301faca730d3ed))
* update mobile donation API and related dependencies ([934d6a3](https://wibugit.wibudev.com/wibu/hipmi/commit/934d6a3ef1015367bee85779796df4f11c5e779c))
## [1.5.40](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.39...v1.5.40) (2026-02-06)
## [1.5.39](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.38...v1.5.39) (2026-01-30)

View File

@@ -1,5 +1,5 @@
File utama: src/app/api/mobile/admin/investment/[id]/investor/route.ts
File utama: src/app/api/mobile/admin/forum/[id]/comment/route.ts
Terapkan pagination pada file "File utama" pada method GET
Analisa juga file "File utama", jika belum memiliki page dari seachParams maka terapkan. Juga pastikan take dan skip sudah sesuai dengan pagination. Buat default nya menjadi 10 untuk take data

View File

@@ -3,12 +3,20 @@ const nextConfig = {
reactStrictMode: false,
experimental: {
serverActions: true,
serverComponentsExternalPackages: ['@prisma/client'],
},
output: "standalone",
staticPageGenerationTimeout: 180, // tingkatkan menjadi 3 menit
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config, { isServer }) => {
if (isServer) {
config.externals = config.externals || [];
config.externals.push('@prisma/client');
}
return config;
},
// async headers() {
// return [
// {

View File

@@ -1,15 +1,16 @@
{
"name": "hipmi",
"version": "1.5.40",
"version": "1.6.6",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"
},
"scripts": {
"dev": "next dev --experimental-https",
"build": "next build",
"build:dev": "next build",
"build": "prisma generate && next build",
"build:dev": "prisma generate && next build",
"start": "next start",
"postbuild": "node scripts/postbuild.js",
"lint": "next lint",
"ver": "bunx commit-and-tag-version -- --prerelease"
},

View File

@@ -4,7 +4,7 @@
generator client {
provider = "prisma-client-js"
engineType = "binary"
binaryTargets = ["native"]
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
datasource db {

60
scripts/postbuild.js Normal file
View File

@@ -0,0 +1,60 @@
const fs = require('fs');
const path = require('path');
const standaloneDir = path.join(__dirname, '../.next/standalone');
const prismaDir = path.join(__dirname, '../node_modules/.prisma');
console.log('🚀 Running postbuild script...');
// Copy Prisma binaries ke standalone output
if (fs.existsSync(prismaDir)) {
const dest = path.join(standaloneDir, 'node_modules/.prisma');
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(prismaDir, dest, { recursive: true });
console.log('✓ Prisma binaries copied to standalone output');
} else {
console.warn('⚠ Prisma binaries directory not found, skipping...');
}
// Copy schema.prisma jika diperlukan
const schemaSrc = path.join(__dirname, '../prisma/schema.prisma');
const schemaDest = path.join(standaloneDir, 'prisma/schema.prisma');
if (fs.existsSync(schemaSrc)) {
fs.mkdirSync(path.dirname(schemaDest), { recursive: true });
fs.copyFileSync(schemaSrc, schemaDest);
console.log('✓ schema.prisma copied to standalone output');
} else {
console.warn('⚠ schema.prisma not found, skipping...');
}
// Copy prisma client dari node_modules/@prisma/client
const prismaClientSrc = path.join(__dirname, '../node_modules/@prisma/client');
const prismaClientDest = path.join(standaloneDir, 'node_modules/@prisma/client');
if (fs.existsSync(prismaClientSrc)) {
fs.mkdirSync(path.dirname(prismaClientDest), { recursive: true });
fs.cpSync(prismaClientSrc, prismaClientDest, { recursive: true });
console.log('✓ @prisma/client copied to standalone output');
} else {
console.warn('⚠ @prisma/client not found, skipping...');
}
// Copy .env file jika ada (untuk production)
const envSrc = path.join(__dirname, '../.env');
const envDest = path.join(standaloneDir, '.env');
if (fs.existsSync(envSrc)) {
fs.copyFileSync(envSrc, envDest);
console.log('✓ .env file copied to standalone output');
} else {
console.warn('⚠ .env file not found, skipping...');
console.warn(' Pastikan DATABASE_URL di-set di system environment server!');
}
// Copy .env-local file jika ada (opsional)
const envLocalSrc = path.join(__dirname, '../.env-local');
const envLocalDest = path.join(standaloneDir, '.env-local');
if (fs.existsSync(envLocalSrc)) {
fs.copyFileSync(envLocalSrc, envLocalDest);
console.log('✓ .env-local file copied to standalone output');
}
console.log('✅ Build script completed!');

View File

@@ -14,8 +14,6 @@ export async function POST(req: Request) {
try {
const { data } = await req.json();
console.log("data >>", data);
const cekUsername = await prisma.user.findUnique({
where: {
username: data.username,
@@ -29,12 +27,12 @@ export async function POST(req: Request) {
});
// ✅ Validasi wajib setuju Terms
if (data.termsOfServiceAccepted !== true) {
return NextResponse.json({
success: false,
message: "You must agree to the Terms of Service",
});
}
// if (data.termsOfServiceAccepted !== true) {
// return NextResponse.json({
// success: false,
// message: "You must agree to the Terms of Service",
// });
// }
const createUser = await prisma.user.create({
data: {

View File

@@ -7,6 +7,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -14,9 +15,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const category = searchParams.get("category");
let fixData;
try {

View File

@@ -1,15 +1,16 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;
@@ -60,7 +61,7 @@ export async function GET(
message: "Success get list report posting",
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("[ERROR GET LIST REPORT POSTING]", error);
@@ -70,7 +71,7 @@ export async function GET(
message: "Error get list report posting",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -80,7 +80,11 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
_count: {
select: {
Forum_ReportPosting: true,
Forum_Komentar: true,
Forum_Komentar: {
where: {
isActive: true,
},
},
},
},
},
@@ -140,6 +144,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Posting id
const countByPostingId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Posting?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -152,10 +164,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByPostingId[item.Forum_Posting?.id] || 0,
}));
} else if (category === "report_comment") {
const data = await prisma.forum_ReportKomentar.findMany({
take: page ? takeData : undefined,
@@ -194,6 +212,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Komentar id
const countByKomentarId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Komentar?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -206,10 +232,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByKomentarId[item.Forum_Komentar?.id] || 0,
}));
} else {
return NextResponse.json(
{
@@ -217,7 +249,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: "Invalid category",
reason: "Invalid category",
},
{ status: 400 }
{ status: 400 },
);
}
@@ -227,7 +259,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Success get data forum ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -236,7 +268,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Error get data forum ${category}`,
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -76,15 +76,27 @@ export async function GET(req: Request) {
data: user,
});
} catch (error) {
console.error("Error in user validation:", error);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : 'No stack';
// Log detailed error for debugging
console.error("❌ [USER-VALIDATE] Error:", errorMsg);
console.error("❌ [USER-VALIDATE] Stack:", errorStack);
console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString());
// Check if it's a database connection error
if (errorMsg.includes("Prisma") || errorMsg.includes("database") || errorMsg.includes("connection")) {
console.error("❌ [USER-VALIDATE] Database connection error detected!");
console.error("❌ [USER-VALIDATE] DATABASE_URL exists:", !!process.env.DATABASE_URL);
}
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan pada server",
error: process.env.NODE_ENV === 'development' ? errorMsg : 'Internal server error',
},
{ status: 500 }
);
}
// Removed prisma.$disconnect() from here to prevent connection pool exhaustion
// Prisma connections are handled globally and shouldn't be disconnected on each request
}

View File

@@ -28,13 +28,10 @@ export default function Login({ version }: { version: string }) {
const [countryCode, setCountryCode] = useState<string>("62"); // default ke Indonesia
async function onLogin() {
console.log("phone >>", phone);
const nomor = phone;
if (nomor.length <= 4) return setError(true);
const fixPhone = `${countryCode}${nomor}`;
console.log("fixPhone >>", fixPhone);
try {
setLoading(true);
@@ -46,7 +43,6 @@ export default function Login({ version }: { version: string }) {
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
console.log("respone >>", respone);
ComponentGlobal_NotifikasiPeringatan(respone?.message);
}
} catch (error) {
@@ -108,9 +104,6 @@ export default function Login({ version }: { version: string }) {
// Simpan hasil akhir
setCountryCode(dialCode);
setPhone(localNumber);
// console.log("Country Code:", dialCode);
// console.log("Clean Local Number:", localNumber);
}}
/>

View File

@@ -25,15 +25,21 @@ export default function WaitingRoom_View({
const [isLoadingHome, setIsLoadingHome] = useState(false);
async function onClickLogout() {
setLoading(true);
const res = await fetch(`/api/auth/logout?id=${userLoginId}`, {
method: "GET",
});
try {
setLoading(true);
const res = await fetch(`/api/auth/logout?id=${userLoginId}`, {
method: "GET",
});
const result = await res.json();
if (res.status === 200) {
ComponentGlobal_NotifikasiBerhasil(result.message);
router.push("/", { scroll: false });
const result = await res.json();
if (res.status === 200) {
ComponentGlobal_NotifikasiBerhasil(result.message);
router.push("/", { scroll: false });
}
} catch (error) {
console.error("Error button to home", error);
} finally {
setLoading(false);
}
}
@@ -83,7 +89,8 @@ export default function WaitingRoom_View({
</Text>
<Text fw={"bold"} c={"white"} align="center">
Harap tunggu, Anda akan menerima pemberitahuan melalui
Whatsapp setelah disetujui.
Whatsapp setelah disetujui, untuk sementara anda bisa
menunggu pada halaman ini atau keluar.
</Text>
</Stack>
{isAccess && (
@@ -110,6 +117,10 @@ export default function WaitingRoom_View({
Home
</Button>
)}
<Button color="red" loading={loading} onClick={onClickLogout}>
Keluar
</Button>
</Stack>
)}
</ComponentGlobal_CardStyles>

View File

@@ -1,24 +1,25 @@
// lib/firebase-admin.ts
import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
import { getMessaging, Messaging } from 'firebase-admin/messaging';
// Ambil dari environment
const serviceAccount = {
projectId: process.env.FIREBASE_ADMIN_PROJECT_ID,
clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'),
};
function getAdminApp() {
if (getApps().length > 0) return getApp();
if (!serviceAccount.projectId || !serviceAccount.clientEmail || !serviceAccount.privateKey) {
throw new Error('Firebase Admin credentials are missing in environment variables');
const privateKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n');
const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID;
const clientEmail = process.env.FIREBASE_ADMIN_CLIENT_EMAIL;
if (!projectId || !clientEmail || !privateKey) {
throw new Error('Firebase Admin credentials are missing in environment variables');
}
return initializeApp({
credential: cert({ projectId, clientEmail, privateKey }),
projectId,
});
}
// Inisialisasi hanya sekali
const app = !getApps().length
? initializeApp({
credential: cert(serviceAccount),
projectId: serviceAccount.projectId,
})
: getApp();
export const adminMessaging = getMessaging(app);
export const adminMessaging: Pick<Messaging, 'send' | 'sendEachForMulticast'> = {
send: (message) => getMessaging(getAdminApp()).send(message),
sendEachForMulticast: (message) => getMessaging(getAdminApp()).sendEachForMulticast(message),
};

188
src/lib/prisma-retry.ts Normal file
View File

@@ -0,0 +1,188 @@
import { prisma } from './prisma';
/**
* Retry configuration for database operations
*/
interface RetryConfig {
maxRetries: number;
initialDelay: number;
maxDelay: number;
factor: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelay: 100,
maxDelay: 5000,
factor: 2,
};
/**
* Check if error is retryable (transient error)
*/
function isRetryableError(error: any): boolean {
const errorMsg = error instanceof Error ? error.message : '';
// Retry on connection-related errors
const retryablePatterns = [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'connection closed',
'connection terminated',
'connection timeout',
'socket hang up',
'too many connections',
'pool is full',
'server login has been failing',
'FATAL:',
'PrismaClientUnknownRequestError',
];
return retryablePatterns.some(pattern =>
errorMsg.toLowerCase().includes(pattern.toLowerCase())
);
}
/**
* Execute database operation with retry mechanism
*
* @param operation - The database operation to execute
* @param config - Retry configuration (optional)
* @param operationName - Name of the operation for logging
*
* @example
* const user = await withRetry(
* () => prisma.user.findUnique({ where: { id: '123' } }),
* undefined,
* 'findUser'
* );
*/
export async function withRetry<T>(
operation: () => Promise<T>,
config?: Partial<RetryConfig>,
operationName?: string
): Promise<T> {
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
let lastError: any;
for (let attempt = 1; attempt <= retryConfig.maxRetries; attempt++) {
try {
const result = await operation();
// Log success if it was a retry
if (attempt > 1 && operationName) {
console.log(`✅ [DB-RETRY] ${operationName} succeeded after ${attempt} attempts`);
}
return result;
} catch (error) {
lastError = error;
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
// Check if we should retry
if (attempt < retryConfig.maxRetries && isRetryableError(error)) {
// Calculate delay with exponential backoff + jitter
const delay = Math.min(
retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt - 1),
retryConfig.maxDelay
);
const jitter = Math.random() * 0.3 * delay; // Add 30% jitter
if (operationName) {
console.warn(
`⚠️ [DB-RETRY] ${operationName} failed (attempt ${attempt}/${retryConfig.maxRetries}): ${errorMsg}`
);
console.log(`⏳ [DB-RETRY] Retrying in ${Math.round(delay + jitter)}ms...`);
}
await new Promise(resolve => setTimeout(resolve, delay + jitter));
} else {
// Don't retry - either max retries reached or not a retryable error
if (operationName) {
console.error(
`❌ [DB-RETRY] ${operationName} failed after ${attempt} attempts: ${errorMsg}`
);
}
break;
}
}
}
// All retries exhausted, throw the last error
throw lastError;
}
/**
* Execute database operation with timeout
*
* @param operation - The database operation to execute
* @param timeout - Timeout in milliseconds (default: 30000)
* @param operationName - Name of the operation for logging
*/
export async function withTimeout<T>(
operation: () => Promise<T>,
timeout: number = 30000,
operationName?: string
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeout}ms`));
}, timeout);
});
try {
return await Promise.race([operation(), timeoutPromise]);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
if (errorMsg.includes('timed out')) {
if (operationName) {
console.error(`⏱️ [DB-TIMEOUT] ${operationName} timed out after ${timeout}ms`);
}
}
throw error;
}
}
/**
* Combine retry and timeout for robust database operations
*
* @param operation - The database operation to execute
* @param options - Retry and timeout options
* @param operationName - Name of the operation for logging
*/
export async function withRetryAndTimeout<T>(
operation: () => Promise<T>,
options?: {
retry?: Partial<RetryConfig>;
timeout?: number;
},
operationName?: string
): Promise<T> {
return withRetry(
() => withTimeout(operation, options?.timeout, operationName),
options?.retry,
operationName
);
}
/**
* Health check for database connection
*/
export async function checkDatabaseConnection(): Promise<boolean> {
try {
await withTimeout(
() => prisma.$queryRaw`SELECT 1`,
5000,
'healthCheck'
);
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error('❌ [DB-HEALTH] Database connection check failed:', errorMsg);
return false;
}
}
export { prisma };

View File

@@ -1,45 +1,21 @@
import { PrismaClient } from "@prisma/client";
// Deklarasikan variabel global untuk menandai apakah listener sudah ditambahkan
declare global {
var prisma: PrismaClient;
var prismaListenersAdded: boolean; // Flag untuk menandai listener
var prisma: PrismaClient | undefined;
}
let prisma: PrismaClient;
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient({
// Reduce logging in production to improve performance
log: ['error', 'warn'],
});
} else {
if (!global.prisma) {
global.prisma = new PrismaClient({
log: ['error', 'warn', 'info', 'query'], // More verbose logging in development
});
}
prisma = global.prisma;
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is required but not found in environment variables");
}
// Tambahkan listener hanya jika belum ditambahkan sebelumnya
if (!global.prismaListenersAdded) {
// Handle graceful shutdown
process.on("SIGINT", async () => {
console.log("Received SIGINT signal. Closing database connections...");
await prisma.$disconnect();
process.exit(0);
const prisma =
global.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"],
});
process.on("SIGTERM", async () => {
console.log("Received SIGTERM signal. Closing database connections...");
await prisma.$disconnect();
process.exit(0);
});
// Tandai bahwa listener sudah ditambahkan
global.prismaListenersAdded = true;
}
// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances)
global.prisma = prisma;
export default prisma;
export { prisma };

View File

@@ -65,11 +65,7 @@ export const middleware = async (req: NextRequest) => {
const { pathname } = req.nextUrl;
const apiBaseUrl = new URL(req.url).origin || process.env.NEXT_PUBLIC_API_URL;
// Removed excessive logging that was causing high CPU usage
// const dbUrl = process.env.DATABASE_URL;
// console.log("DATABASE_URL >>", dbUrl);
// console.log("URL Access >>", req.url);
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || new URL(req.url).origin;
// Handle CORS preflight
const corsResponse = handleCors(req);

4
types/env.d.ts vendored
View File

@@ -11,5 +11,9 @@ declare namespace NodeJS {
NEXT_PUBLIC_BASE_SESSION_KEY?: string;
RESEND_APIKEY?: string;
WA_SERVER_TOKEN?: string;
FIREBASE_ADMIN_PRIVATE_KEY?: string;
FIREBASE_ADMIN_CLIENT_EMAIL?: string;
FIREBASE_ADMIN_PROJECT_ID?: string;
NEXT_PUBLIC_API_URL?: string;
}
}