Compare commits

...

15 Commits

Author SHA1 Message Date
e7698618a2 Merge pull request 'server/14-apr-26' (#69) from server/14-apr-26 into staging
Reviewed-on: #69
2026-04-14 16:38:23 +08:00
a821aeb129 chore: remove unused test file
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:42:58 +08:00
0dd939b979 chore(release): 1.7.5 2026-04-14 15:34:52 +08:00
38239c52d6 fix: override MIME type for PDF uploads & fix build errors
- Fix PDF upload failing on Android due to wrong MIME type (image/pdf → application/pdf)
- Add safe JSON parsing for external storage responses with incorrect Content-Type headers
- Add detailed upload logging for debugging (file name, type, size, response status)
- Fix TypeScript error: missing prisma import in forum preview-report-posting route
- Fix ESLint warning: unescaped apostrophe in support-center page
- Add DEBUG_UPLOAD_FILE.md documentation for troubleshooting guide

Fixes upload issue where Android devices couldn't upload PDF files due to MIME type mismatch

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:17:45 +08:00
a71997b4ef Fix version 1.7.4
## no issue
2026-03-30 15:49:07 +08:00
0f584f8c72 chore(release): 1.7.4 2026-03-30 15:48:42 +08:00
445801941d fix 2026-03-27 17:59:59 +08:00
defafe694f chore(release): 1.7.3 2026-03-27 17:22:38 +08:00
cd3a9cc223 Fix server
### Issue:  process.listenerCount()
2026-03-13 16:44:25 +08:00
0eb31073b7 chore(release): 1.7.2 2026-03-13 16:43:35 +08:00
d33296d23b Fix validasi mobile
### No issue;
2026-03-11 14:12:07 +08:00
f7d05783c7 chore(release): 1.7.1 2026-03-11 13:56:09 +08:00
9199cc6971 Merge pull request 'fix-server' (#68) from fix-server/5-mar-26 into staging
Reviewed-on: #68
2026-03-05 16:44:21 +08:00
74d90ea6aa Merge pull request 'fix-server/4-mar-26' (#67) from fix-server/4-mar-26 into staging
Reviewed-on: #67
2026-03-04 16:44:05 +08:00
edc9d532d4 Merge pull request 'bug-prisma 4' (#66) from bug-prisma/3-mar-26 into staging
Reviewed-on: #66
2026-03-03 16:47:49 +08:00
10 changed files with 431 additions and 41 deletions

View File

@@ -2,6 +2,21 @@
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.7.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.4...v1.7.5) (2026-04-14)
### Bug Fixes
* override MIME type for PDF uploads & fix build errors ([38239c5](https://wibugit.wibudev.com/wibu/hipmi/commit/38239c52d6d083d0c6ee6fde94f09e16fa7201dd))
## [1.7.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.3...v1.7.4) (2026-03-30)
## [1.7.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.2...v1.7.3) (2026-03-27)
## [1.7.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.1...v1.7.2) (2026-03-13)
## [1.7.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.0...v1.7.1) (2026-03-11)
## [1.7.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.9...v1.7.0) (2026-03-10)

170
DEBUG_UPLOAD_FILE.md Normal file
View File

@@ -0,0 +1,170 @@
# Debug Guide: Upload File Android vs iOS
## 📱 Problem
- ✅ Upload **IMAGE** berhasil di iOS dan Android
- ❌ Upload **PDF** gagal di Android dengan error: `Status: 400 Bad Request`
## 🔍 Root Cause (DITEMUKAN!)
### **Masalah MIME Type PDF**
Dari log upload:
```
File details:
- Name: 154ce3b0-6fc0-4a39-9e09-3f9aa2b19300.pdf
- Type: image/pdf ← ❌ SALAH!
- Size: 26534 bytes
```
**Yang benar:**
-`image/pdf` (salah - tidak ada MIME type ini)
-`application/pdf` (benar - standard MIME type untuk PDF)
### **Kenapa Terjadi?**
Mobile app (Android) salah set MIME type saat mengirim file PDF. Kemungkinan:
1. File picker/set MIME type salah di mobile code
2. Android WebView auto-detect MIME type incorrectly
3. Mobile app hardcoded MIME type yang salah
## 🛠️ Solusi yang Sudah Diterapkan
### File: `src/app/api/mobile/file/route.ts`
**Fix #1: Safe JSON Parsing**
- ✅ Cek response sebagai text dulu, lalu parse JSON
- ✅ Handle Content-Type yang salah dari external storage
**Fix #2: MIME Type Override (LATEST)**
- ✅ Deteksi file PDF dari extension (.pdf)
- ✅ Override MIME type ke `application/pdf` jika salah
- ✅ Rebuild FormData dengan file yang sudah difix
**Code:**
```typescript
// Jika file PDF tapi type bukan application/pdf, fix it
if (fileName.endsWith(".pdf") && originalType !== "application/pdf") {
console.log("⚠️ WARNING: PDF file has wrong MIME type:", originalType);
console.log("🔧 Overriding to: application/pdf");
// Create new File with correct MIME type
const buffer = await file.arrayBuffer();
fixedFile = new File([buffer], file.name, {
type: "application/pdf",
lastModified: file.lastModified,
});
// Rebuild formData with fixed file
formData.set("file", fixedFile);
}
```
## 🧪 Cara Testing
### 1. **Test Upload dari Android**
Coba upload file PDF dari Android dan perhatikan log di terminal:
```bash
# Log yang akan muncul:
=== UPLOAD REQUEST START ===
dirId: xxx
File details:
- Name: dokumen.pdf
- Type: application/pdf
- Size: 1234567 bytes
- Size (KB): 1205.63
===========================
Directory key: xxx
=== EXTERNAL STORAGE RESPONSE ===
Status: 400
Status Text: Bad Request
Content-Type: text/html; charset=utf-8
=================================
=== ERROR: Non-JSON Response ===
Response text: Unsupported file format...
=================================
```
### 2. **Informasi yang Perlu Dicari:**
Dari log di atas, perhatikan:
- **File size** → Berapa MB? (mungkin terlalu besar?)
- **File type** → `application/pdf` atau yang lain?
- **External storage response** → Status code & message?
- **Error text** → Apa yang dikembalikan server external?
### 3. **Compare iOS vs Android**
Upload file yang sama dari iOS dan Android, bandingkan log-nya.
## 📊 Expected Log Output
### ✅ Success Case:
```
=== UPLOAD REQUEST START ===
dirId: investment
File details:
- Name: proposal.pdf
- Type: application/pdf
- Size: 524288 bytes
- Size (KB): 512.00
===========================
Directory key: investment
=== EXTERNAL STORAGE RESPONSE ===
Status: 200
Status Text: OK
Content-Type: application/json
=================================
✅ Upload SUCCESS
```
### ❌ Failed Case (Non-JSON Response):
```
=== UPLOAD REQUEST START ===
dirId: investment
File details:
- Name: proposal.pdf
- Type: application/pdf
- Size: 5242880 bytes ← Mungkin terlalu besar?
- Size (KB): 5120.00
===========================
=== EXTERNAL STORAGE RESPONSE ===
Status: 413 ← Payload Too Large?
Content-Type: text/html
=================================
=== ERROR: Non-JSON Response ===
Response text: <html><body>413 Request Entity Too Large</body></html>
=================================
```
## 🔧 Next Steps (Setelah Testing)
Berdasarkan log, kita bisa identify masalahnya:
### **Jika masalah FILE SIZE:**
- Tambahkan limit validation di frontend
- Compress PDF sebelum upload
- Increase external storage limit
### **Jika masalah FILE FORMAT:**
- Validate file type sebelum upload
- Convert format jika perlu
- Update external storage allowed formats
### **Jika masalah NETWORK/HEADERS:**
- Check user-agent differences
- Validate Authorization header
- Check CORS settings
## 📝 Checklist Testing
- [ ] Test upload PDF kecil (< 1MB) dari Android
- [ ] Test upload PDF besar (> 5MB) dari Android
- [ ] Test upload PDF dari iOS (baseline)
- [ ] Compare log output iOS vs Android
- [ ] Check file type yang dikirim
- [ ] Check file size yang dikirim
- [ ] Check response dari external storage
## 🎯 Goal
Dari log yang detail, kita bisa tahu **exact reason** kenapa Android fail, lalu fix dengan tepat.
---
**Last Updated:** 2026-04-14
**Status:** ✅ Logging added, ready for testing

View File

@@ -1,6 +1,6 @@
{
"name": "hipmi",
"version": "1.7.0",
"version": "1.7.5",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"

View File

@@ -98,7 +98,7 @@ export default function SupportCenter() {
<Title>Support Center</Title>
</Group>
<Text align="center">
Send us a message and we'll get back to you as soon as possible.
Send us a message and we&apos;ll get back to you as soon as possible.
</Text>
</Stack>
</Stack>

View File

@@ -1,8 +1,12 @@
import { sessionCreate } from "@/app/(auth)/_lib/session_create";
import prisma from "@/lib/prisma";
import backendLogger from "@/util/backendLogger";
import { NextResponse } from "next/server";
/**
* Validasi OTP untuk login mobile
* @param req - Request dengan body { nomor: string, code: string }
* @returns Response dengan token jika OTP valid
*/
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
@@ -12,8 +16,21 @@ export async function POST(req: Request) {
}
try {
const { nomor } = await req.json();
const { nomor, code } = await req.json();
// Validasi input: nomor dan code wajib ada
if (!nomor || !code) {
return NextResponse.json(
{ success: false, message: "Nomor dan kode OTP wajib diisi" },
{ status: 400 }
);
}
// Special case untuk Apple Review: nomor 6282340374412 dengan code "1234" selalu valid
const isAppleReviewNumber = nomor === "6282340374412";
const isAppleReviewCode = code === "1234";
// Cek user berdasarkan nomor
const dataUser = await prisma.user.findUnique({
where: {
nomor: nomor,
@@ -28,11 +45,92 @@ export async function POST(req: Request) {
},
});
if (dataUser == null)
if (dataUser == null) {
return NextResponse.json(
{ success: false, message: "Nomor Belum Terdaftar" },
{ status: 200 }
);
}
// Validasi OTP (skip untuk Apple Review number di production)
let otpValid = false;
if (isAppleReviewNumber && isAppleReviewCode) {
// Special case: Apple Review number dengan code "1234" selalu valid
otpValid = true;
console.log("Apple Review login bypass untuk nomor: " + nomor);
} else {
// Normal flow: validasi OTP dari database
const otpRecord = await prisma.kodeOtp.findFirst({
where: {
nomor: nomor,
isActive: true,
},
orderBy: {
createdAt: "desc",
},
});
if (!otpRecord) {
return NextResponse.json(
{ success: false, message: "Kode OTP tidak ditemukan" },
{ status: 400 }
);
}
// Cek expired OTP (5 menit dari createdAt)
const now = new Date();
const otpCreatedAt = new Date(otpRecord.createdAt);
const expiredTime = new Date(otpCreatedAt.getTime() + 5 * 60 * 1000); // 5 menit
if (now > expiredTime) {
// OTP sudah expired, update isActive menjadi false
await prisma.kodeOtp.updateMany({
where: {
nomor: nomor,
isActive: true,
},
data: {
isActive: false,
},
});
return NextResponse.json(
{ success: false, message: "Kode OTP sudah kadaluarsa" },
{ status: 400 }
);
}
// Validasi code OTP
const inputCode = parseInt(code);
if (isNaN(inputCode) || inputCode !== otpRecord.otp) {
return NextResponse.json(
{ success: false, message: "Kode OTP tidak valid" },
{ status: 400 }
);
}
otpValid = true;
// Nonaktifkan OTP yang sudah digunakan
await prisma.kodeOtp.updateMany({
where: {
nomor: nomor,
isActive: true,
},
data: {
isActive: false,
},
});
}
// Generate token jika OTP valid
if (!otpValid) {
return NextResponse.json(
{ success: false, message: "Validasi OTP gagal" },
{ status: 400 }
);
}
const token = await sessionCreate({
sessionKey: process.env.NEXT_PUBLIC_BASE_SESSION_KEY!,
@@ -46,6 +144,7 @@ export async function POST(req: Request) {
{ status: 500 }
);
}
// Buat response dengan token dalam cookie
const response = NextResponse.json(
{
@@ -69,7 +168,7 @@ export async function POST(req: Request) {
return response;
} catch (error) {
backendLogger.log("API Error or Server Error", error);
console.log("API Error or Server Error", error);
return NextResponse.json(
{
success: false,

View File

@@ -2,14 +2,50 @@ import { funGetDirectoryNameByValue } from "@/app_modules/_global/fun/get";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const formData = await request.formData();
const dirId = formData.get("dirId");
const keyOfDirectory = await funGetDirectoryNameByValue({
value: dirId as string,
});
try {
const formData = await request.formData();
const dirId = formData.get("dirId");
const file = formData.get("file") as File | null;
// === LOGGING: Request Details ===
console.log("=== UPLOAD REQUEST START ===");
console.log("dirId:", dirId);
console.log("File details:");
console.log(" - Name:", file?.name);
console.log(" - Type:", file?.type);
console.log(" - Size:", file?.size, "bytes");
console.log(" - Size (KB):", file ? (file.size / 1024).toFixed(2) : "N/A");
console.log("===========================");
// FIX: Override MIME type jika salah (mobile app kadang kirim image/pdf)
let fixedFile = file;
if (file) {
const fileName = file.name.toLowerCase();
const originalType = file.type.toLowerCase();
// Jika file PDF tapi type bukan application/pdf, fix it
if (fileName.endsWith(".pdf") && originalType !== "application/pdf") {
console.log("⚠️ WARNING: PDF file has wrong MIME type:", originalType);
console.log("🔧 Overriding to: application/pdf");
// Create new File with correct MIME type
const buffer = await file.arrayBuffer();
fixedFile = new File([buffer], file.name, {
type: "application/pdf",
lastModified: file.lastModified,
});
// Rebuild formData with fixed file
formData.set("file", fixedFile);
}
}
const keyOfDirectory = await funGetDirectoryNameByValue({
value: dirId as string,
});
console.log("Directory key:", keyOfDirectory);
const res = await fetch("https://wibu-storage.wibudev.com/api/upload", {
method: "POST",
body: formData,
@@ -18,9 +54,70 @@ export async function POST(request: Request) {
},
});
const dataRes = await res.json();
// === LOGGING: Response Details ===
console.log("=== EXTERNAL STORAGE RESPONSE ===");
console.log("Status:", res.status);
console.log("Status Text:", res.statusText);
console.log("Content-Type:", res.headers.get("content-type"));
console.log("=================================");
// Cek content-type sebelum parse JSON
const contentType = res.headers.get("content-type") || "";
let dataRes;
// Try parse JSON untuk semua response (beberapa server salah set content-type)
try {
const rawResponse = await res.text();
// Coba parse sebagai JSON
try {
dataRes = JSON.parse(rawResponse);
console.log("✅ Successfully parsed response as JSON");
} catch {
// Bukan JSON - gunakan raw text
console.log("⚠️ Response is not JSON, using raw text");
if (res.ok) {
// Success tapi bukan JSON - return success response
return NextResponse.json(
{
success: true,
message: "Success upload file " + keyOfDirectory,
},
{ status: 200 }
);
} else {
return NextResponse.json(
{
success: false,
message: "Upload failed",
error: rawResponse.substring(0, 500),
fileDetails: {
name: file?.name,
type: file?.type,
size: file?.size,
}
},
{ status: res.status || 400 }
);
}
}
} catch (readError) {
console.log("❌ Failed to read response body");
console.log("Read error:", (readError as Error).message);
return NextResponse.json(
{
success: false,
message: "Failed to read response",
reason: (readError as Error).message,
},
{ status: 500 }
);
}
if (res.ok) {
console.log("✅ Upload SUCCESS");
return NextResponse.json(
{
success: true,
@@ -30,20 +127,34 @@ export async function POST(request: Request) {
{ status: 200 }
);
} else {
const errorText = await res.text();
console.log(`Failed upload ${keyOfDirectory}: ${errorText}`);
console.log("❌ Upload FAILED");
console.log("Response:", dataRes);
const errorMessage = dataRes.message || dataRes.error || JSON.stringify(dataRes);
return NextResponse.json(
{ success: false, message: errorText },
{ status: 400 }
{
success: false,
message: errorMessage || "Upload failed",
fileDetails: {
name: file?.name,
type: file?.type,
size: file?.size,
}
},
{ status: res.status || 400 }
);
}
} catch (error) {
console.log("Error upload >>", (error as Error).message || error);
console.log("=== CATCH ERROR ===");
console.log("Error:", (error as Error).message);
console.log("Stack:", (error as Error).stack);
console.log("===================");
return NextResponse.json(
{
success: false,
message: "Failed upload file",
reason: (error as Error).message || error,
reason: (error as Error).message || "Unknown error",
},
{ status: 500 }
);

View File

@@ -1,3 +1,4 @@
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
export async function GET(

View File

@@ -2,7 +2,10 @@ import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
import { NotificationMobileBodyType, NotificationMobileTitleType } from "../../../../../types/type-mobile-notification";
import {
NotificationMobileBodyType,
NotificationMobileTitleType,
} from "../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
export { POST, GET };
@@ -72,15 +75,9 @@ async function GET(request: Request) {
const search = searchParams.get("search");
const category = searchParams.get("category");
const page = searchParams.get("page");
const takeData = 5;
const takeData = 10;
const skipData = (Number(page) - 1) * takeData;
// console.log("authorId", authorId);
// console.log("userLoginId", userLoginId);
// console.log("search", search);
// console.log("category", category);
console.log("page", page);
try {
if (category === "beranda") {
const blockUserId = await prisma.blockedUser

View File

@@ -45,10 +45,16 @@ async function gracefulShutdown(): Promise<void> {
console.log("[Prisma] Semua koneksi ditutup");
}
// Register shutdown handlers (hanya di environment Node.js)
if (typeof process !== "undefined") {
process.on("SIGINT", gracefulShutdown);
process.on("SIGTERM", gracefulShutdown);
// Register shutdown handlers (hanya di environment Node.js server)
// Cegah duplikasi listener dengan cek listenerCount terlebih dahulu
// IMPORTANT: Bungkus dalam check untuk mencegah error di browser
if (typeof process !== "undefined" && typeof process.listenerCount === "function") {
if (process.listenerCount("SIGINT") === 0) {
process.on("SIGINT", gracefulShutdown);
}
if (process.listenerCount("SIGTERM") === 0) {
process.on("SIGTERM", gracefulShutdown);
}
}
export default prisma;

View File

@@ -1,9 +0,0 @@
import { prisma } from "@/lib";
import { describe, test, expect } from "bun:test";
describe("coba test", () => {
test("coba", async () => {
const user = await prisma.user.findMany();
expect(user).not.toBeEmpty();
});
});