diff --git a/.gitignore b/.gitignore index 61b4bc33..924563ab 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ yarn-error.log* # vercel .vercel +# logs +logs + # typescript *.tsbuildinfo next-env.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 30560fe0..56cbce3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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.2.30](https://github.com/bipproduction/hipmi/compare/v1.2.29...v1.2.30) (2024-12-16) + +## [1.2.29](https://github.com/bipproduction/hipmi/compare/v1.2.28...v1.2.29) (2024-12-13) + +## [1.2.28](https://github.com/bipproduction/hipmi/compare/v1.2.27...v1.2.28) (2024-12-12) + ## [1.2.27](https://github.com/bipproduction/hipmi/compare/v1.2.26...v1.2.27) (2024-12-12) ## [1.2.26](https://github.com/bipproduction/hipmi/compare/v1.2.25...v1.2.26) (2024-12-12) diff --git a/bun.lockb b/bun.lockb index 88eaa6ad..f486e079 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f18b9dad..5091e4d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hipmi", - "version": "1.2.27", + "version": "1.2.30", "private": true, "prisma": { "seed": "npx tsx prisma/seed.ts --yes" @@ -50,6 +50,7 @@ "autoprefixer": "10.4.14", "bufferutil": "^4.0.8", "bun": "^1.1.38", + "date-fns": "^4.1.0", "dayjs": "^1.11.10", "dotenv": "^16.4.5", "echarts": "^5.4.3", @@ -95,6 +96,8 @@ "wibu": "bipproduction/wibu", "wibu-cli": "^1.0.91", "wibu-pkg": "^1.0.3", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", "yaml": "^2.3.2" } } diff --git a/src/app/api/image/delete/route.ts b/src/app/api/image/delete/route.ts new file mode 100644 index 00000000..a38d0e37 --- /dev/null +++ b/src/app/api/image/delete/route.ts @@ -0,0 +1,53 @@ +import { funGetDirectoryNameByValue } from "@/app_modules/_global/fun/get"; +import backendLogger from "@/util/backendLogger"; +import { NextResponse } from "next/server"; + +export async function DELETE(req: Request) { + const data = await req.json(); + const id = data.fileId; + const dirId = data.dirId; + + const keyOfDirectory = await funGetDirectoryNameByValue({ + value: dirId, + }); + + if (req.method === "DELETE") { + try { + const res = await fetch( + `https://wibu-storage.wibudev.com/api/files/${id}/delete`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${process.env.WS_APIKEY}`, + }, + } + ); + backendLogger.info("Server status code: " + res.status); + const data = await res.json(); + if (res.ok) { + backendLogger.info( + `Success delete ${keyOfDirectory}` + ); + return NextResponse.json({ success: true }); + } else { + const errorText = await res.json(); + backendLogger.error( + `Failed delete ${keyOfDirectory}: ` + errorText.message + ); + return NextResponse.json({ + success: false, + message: errorText.message, + }); + } + } catch (error) { + backendLogger.error(`Delete error ${keyOfDirectory}:`, error); + return NextResponse.json({ + success: false, + message: "An unexpected error occurred", + }); + } + } else { + backendLogger.error(`Error upload ${keyOfDirectory}: Method not allowed`); + return NextResponse.json({ success: false, message: "Method not allowed" }); + } +} diff --git a/src/app/api/image/upload/route.ts b/src/app/api/image/upload/route.ts new file mode 100644 index 00000000..a1004088 --- /dev/null +++ b/src/app/api/image/upload/route.ts @@ -0,0 +1,58 @@ +import { funGetDirectoryNameByValue } from "@/app_modules/_global/fun/get"; +import backendLogger from "@/util/backendLogger"; +import { NextResponse } from "next/server"; +export async function POST(request: Request) { + const formData = await request.formData(); + + const valueOfDir = formData.get("dirId"); + const keyOfDirectory = await funGetDirectoryNameByValue({ + value: valueOfDir as string, + }); + + if (request.method === "POST") { + try { + const res = await fetch("https://wibu-storage.wibudev.com/api/upload", { + method: "POST", + body: formData, + headers: { + Authorization: `Bearer ${process.env.WS_APIKEY}`, + }, + }); + + backendLogger.info("Server status code: " + res.status); + const dataRes = await res.json(); + + if (res.ok) { + backendLogger.info( + `Success upload ${keyOfDirectory}: ${JSON.stringify(dataRes.data)}` + ); + return NextResponse.json( + { success: true, data: dataRes.data }, + { status: 200 } + ); + } else { + const errorText = await res.text(); + backendLogger.error(`Failed upload ${keyOfDirectory}: ${errorText}`); + return NextResponse.json( + { success: false, message: errorText }, + { status: 400 } + ); + } + } catch (error) { + backendLogger.error(`Error upload ${keyOfDirectory}: ${error}`); + return NextResponse.json( + { + success: false, + message: "An unexpected error occurred", + }, + { status: 500 } + ); + } + } else { + backendLogger.error(`Error upload ${keyOfDirectory}: Method not allowed`); + return NextResponse.json( + { success: false, message: "Method not allowed" }, + { status: 405 } + ); + } +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts deleted file mode 100644 index 00e2b69c..00000000 --- a/src/app/api/upload/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextResponse } from "next/server"; -export async function POST(request: Request) { - const WS_APIKEY = process.env.WS_APIKEY; - console.log(WS_APIKEY); - - try { - const formData = await request.formData(); - - const res = await fetch("https://wibu-storage.wibudev.com/api/upload", { - method: "POST", - body: formData, - headers: { - Authorization: `Bearer ${process.env.WS_APIKEY}`, - }, - }); - - // if (res.ok) { - // console.log("Berhasil"); - // const hasil = await res.json(); - // return { success: true, data: hasil.data }; - // } else { - // const errorText = await res.text(); - // return { success: false, data: {} }; - // } - } catch (error) { - console.log(error); - } - - // try { - // const res = await fetch("https://wibu-storage.wibudev.com/api/upload", { - // method: "POST", - // body: formData, - // headers: { - // Authorization: `Bearer ${process.env.WS_APIKEY}`, - // }, - // }); - - // if (res.ok) { - // const hasil = await res.json(); - // return { success: true, data: hasil.data }; - // } else { - // const errorText = await res.text(); - // return { success: false, data: {} }; - // } - // } catch (error) { - // console.error("Upload error:", error); - // return { success: false, data: {} }; - // } - - return NextResponse.json({ success: true }); -} diff --git a/src/app/zCoba/page.tsx b/src/app/zCoba/page.tsx index 8df38920..dd5cb2f4 100644 --- a/src/app/zCoba/page.tsx +++ b/src/app/zCoba/page.tsx @@ -33,7 +33,7 @@ export default function Page() { const formData = new FormData(); formData.append("file", filePP as any); - const res = await fetch("/api/upload", { + const res = await fetch("/api/image/upload", { method: "POST", body: formData, }); diff --git a/src/app_modules/_global/fun/delete/fun_delete_file_by_id.tsx b/src/app_modules/_global/fun/delete/fun_delete_file_by_id.tsx index a31df82d..cb3d48be 100644 --- a/src/app_modules/_global/fun/delete/fun_delete_file_by_id.tsx +++ b/src/app_modules/_global/fun/delete/fun_delete_file_by_id.tsx @@ -1,24 +1,50 @@ -export async function funGlobal_DeleteFileById({ fileId }: { fileId: string }) { - try { - const res = await fetch( - `https://wibu-storage.wibudev.com/api/files/${fileId}/delete`, - { - method: "DELETE", - headers: { - Authorization: `Bearer ${process.env.WS_APIKEY}`, - }, - } - ); +import { clientLogger } from "@/util/clientLogger"; - if (res.ok) { - const hasil = await res.json(); - return { success: true }; +export async function funGlobal_DeleteFileById({ + fileId, + dirId, +}: { + fileId: string; + dirId?: string; +}) { + try { + const res = await fetch("/api/image/delete", { + method: "DELETE", + body: JSON.stringify({ fileId, dirId }), + }); + + const data = await res.json(); + + if (data.success) { + clientLogger.info(`File ${fileId} deleted successfully`); + return { success: true, message: "File berhasil dihapus" }; } else { - const errorText = await res.json(); - return { success: false }; + return { success: false, message: data.message }; } } catch (error) { - return { success: false }; console.error("Upload error:", error); + return { success: false, message: "An unexpected error occurred" }; } + // try { + // const res = await fetch( + // `https://wibu-storage.wibudev.com/api/files/${fileId}/delete`, + // { + // method: "DELETE", + // headers: { + // Authorization: `Bearer ${process.env.WS_APIKEY}`, + // }, + // } + // ); + + // if (res.ok) { + // const hasil = await res.json(); + // return { success: true, message: "File berhasil dihapus" }; + // } else { + // const errorText = await res.json(); + // return { success: false, message: errorText.message }; + // } + // } catch (error) { + // console.error("Upload error:", error); + // return { success: false, message: "An unexpected error occurred" }; + // } } diff --git a/src/app_modules/_global/fun/get/fun_get_directory_name.ts b/src/app_modules/_global/fun/get/fun_get_directory_name.ts new file mode 100644 index 00000000..fbacfec2 --- /dev/null +++ b/src/app_modules/_global/fun/get/fun_get_directory_name.ts @@ -0,0 +1,11 @@ +import { DIRECTORY_ID } from "@/app/lib"; + +export async function funGetDirectoryNameByValue({ + value, +}: { + value?: string | null; +}) { + if (!value) return null; + const object: any = DIRECTORY_ID; + return Object.keys(object).find((key) => object[key] === value); +} diff --git a/src/app_modules/_global/fun/get/index.ts b/src/app_modules/_global/fun/get/index.ts index b71aacbe..25d8ace9 100644 --- a/src/app_modules/_global/fun/get/index.ts +++ b/src/app_modules/_global/fun/get/index.ts @@ -1,5 +1,5 @@ - import { funGlobal_CheckProfile } from "./fun_check_profile"; +import { funGetDirectoryNameByValue } from "./fun_get_directory_name"; import { funGlobal_getNomorAdmin } from "./fun_get_nomor_admin"; import { funGetUserIdByToken } from "./fun_get_user_id_by_token"; import { funGlobal_getMasterKategoriApp } from "./fun_master_kategori_app"; @@ -8,3 +8,4 @@ export { funGlobal_getMasterKategoriApp }; export { funGlobal_getNomorAdmin }; export { funGetUserIdByToken }; export { funGlobal_CheckProfile }; +export { funGetDirectoryNameByValue }; diff --git a/src/app_modules/_global/fun/upload/fun_upload_to_storage.ts b/src/app_modules/_global/fun/upload/fun_upload_to_storage.ts index df5b7377..4af61f50 100644 --- a/src/app_modules/_global/fun/upload/fun_upload_to_storage.ts +++ b/src/app_modules/_global/fun/upload/fun_upload_to_storage.ts @@ -32,40 +32,20 @@ export async function funGlobal_UploadToStorage({ console.error("File terlalu besar"); return { success: false, message: "File size exceeds limit" }; } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 30000); // Timeout 30 detik - const formData = new FormData(); formData.append("file", file); formData.append("dirId", dirId); - try { - const res = await fetch("https://wibu-storage.wibudev.com/api/upload", { - method: "POST", - body: formData, - headers: { - Authorization: `Bearer ${Env_WS_APIKEY}`, - }, - signal: controller.signal, - }); + const upload = await fetch("/api/image/upload", { + method: "POST", + body: formData, + }); - clearTimeout(timeoutId); // Bersihkan timeout jika selesai tepat waktu + const res = await upload.json(); - if (res.ok) { - const dataRes = await res.json(); - // const cekLog = await res.text(); - // console.log(cekLog); - return { success: true, data: dataRes.data }; - } else { - const errorText = await res.text(); - console.error("Error:", errorText); - return { success: false, message: errorText }; - } - } catch (error) { - clearTimeout(timeoutId); // - - console.error("Error:", error); - return { success: false, message: "An unexpected error occurred" }; + if (upload.ok) { + return { success: true, data: res.data, message: res.message }; + } else { + return { success: false, data: {}, message: res.message }; } } diff --git a/src/app_modules/katalog/profile/create/view.tsx b/src/app_modules/katalog/profile/create/view.tsx index f0dd96b4..90d51ab2 100644 --- a/src/app_modules/katalog/profile/create/view.tsx +++ b/src/app_modules/katalog/profile/create/view.tsx @@ -1,42 +1,16 @@ "use client"; -import { DIRECTORY_ID } from "@/app/lib"; -import { MainColor } from "@/app_modules/_global/color"; -import { - ComponentGlobal_BoxInformation, - ComponentGlobal_BoxUploadImage, - ComponentGlobal_ErrorInput, -} from "@/app_modules/_global/component"; -import { - funGlobal_DeleteFileById, - funGlobal_UploadToStorage, -} from "@/app_modules/_global/fun"; -import { MAX_SIZE } from "@/app_modules/_global/lib"; -import { PemberitahuanMaksimalFile } from "@/app_modules/_global/lib/max_size"; -import { ComponentGlobal_NotifikasiPeringatan } from "@/app_modules/_global/notif_global"; -import { - AspectRatio, - Avatar, - Box, - Button, - Center, - FileButton, - Image, - Paper, - Select, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { IconAt, IconCamera, IconUpload } from "@tabler/icons-react"; +import { ComponentGlobal_ErrorInput } from "@/app_modules/_global/component"; +import { Select, Stack, TextInput } from "@mantine/core"; +import { IconAt } from "@tabler/icons-react"; import { useState } from "react"; import { gmailRegex } from "../../component/regular_expressions"; import { Profile_ComponentCreateNewProfile } from "../_component"; +import Profile_ViewUploadBackground from "./view_upload_background"; +import Profile_ViewUploadFoto from "./view_upload_foto"; export default function CreateProfile() { - const [filePP, setFilePP] = useState(null); const [imgPP, setImgPP] = useState(); - const [fileBG, setFileBG] = useState(null); const [imgBG, setImgBG] = useState(); const [fotoProfileId, setFotoProfileId] = useState(""); const [backgroundProfileId, setBackgroundProfileId] = useState(""); @@ -51,223 +25,19 @@ export default function CreateProfile() { return ( <> - - - -
- {imgPP ? ( - - - - ) : ( - - - - )} -
+ -
- { - try { - const buffer = URL.createObjectURL( - new Blob([new Uint8Array(await files.arrayBuffer())]) - ); - - if (files.size > MAX_SIZE) { - ComponentGlobal_NotifikasiPeringatan( - PemberitahuanMaksimalFile - ); - setImgPP(null); - setFilePP(null); - - return; - } - - if (fotoProfileId != "") { - const deleteFotoProfile = await funGlobal_DeleteFileById({ - fileId: fotoProfileId, - }); - - if (deleteFotoProfile.success) { - setFotoProfileId(""); - - const uploadPhoto = await funGlobal_UploadToStorage({ - file: files, - dirId: DIRECTORY_ID.profile_foto, - }); - - if (uploadPhoto.success) { - setFotoProfileId(uploadPhoto.data.id); - setImgPP(buffer); - setFilePP(files); - } else { - ComponentGlobal_NotifikasiPeringatan( - "Gagal upload foto profile" - ); - } - } - } else { - const uploadPhoto = await funGlobal_UploadToStorage({ - file: files, - dirId: DIRECTORY_ID.profile_foto, - }); - - if (uploadPhoto.success) { - setFotoProfileId(uploadPhoto.data.id); - setImgPP(buffer); - setFilePP(files); - } else { - ComponentGlobal_NotifikasiPeringatan( - "Gagal upload foto profile" - ); - } - } - } catch (error) { - console.log(error); - } - }} - accept="image/png,image/jpeg" - > - {(props) => ( - - )} - -
-
-
- - - - - - {imgBG ? ( - - Foto - - ) : ( - - - - Upload Background - - - )} - - -
- { - try { - const buffer = URL.createObjectURL( - new Blob([new Uint8Array(await files.arrayBuffer())]) - ); - - if (files.size > MAX_SIZE) { - ComponentGlobal_NotifikasiPeringatan( - PemberitahuanMaksimalFile - ); - setImgBG(null); - setFileBG(null); - return; - } - - if (backgroundProfileId != "") { - const deleteFotoBg = await funGlobal_DeleteFileById({ - fileId: backgroundProfileId, - }); - - if (deleteFotoBg.success) { - setBackgroundProfileId(""); - - const uploadBackground = - await funGlobal_UploadToStorage({ - file: files, - dirId: DIRECTORY_ID.profile_background, - }); - - if (uploadBackground.success) { - setBackgroundProfileId(uploadBackground.data.id); - setImgBG(buffer); - setFileBG(files); - } else { - ComponentGlobal_NotifikasiPeringatan( - "Gagal upload background profile" - ); - } - } - } else { - const uploadBackground = await funGlobal_UploadToStorage({ - file: files, - dirId: DIRECTORY_ID.profile_background, - }); - - if (uploadBackground.success) { - setBackgroundProfileId(uploadBackground.data.id); - setImgBG(buffer); - setFileBG(files); - } else { - ComponentGlobal_NotifikasiPeringatan( - "Gagal upload background profile" - ); - } - } - } catch (error) { - console.log(error); - } - }} - accept="image/png,image/jpeg" - > - {(props) => ( - - )} - -
-
-
+ void; + backgroundProfileId: string; + onSetBackgroundProfileId: (id: string) => void; +}) { + const [isLoading, setLoading] = useState(false); + + return ( + <> + + + + + {isLoading ? ( +
+ +
+ ) : imgBG ? ( + + Foto + + ) : ( + + + + Upload Background + + + )} +
+ +
+ { + try { + setLoading(true); + const buffer = URL.createObjectURL( + new Blob([new Uint8Array(await files.arrayBuffer())]) + ); + + if (files.size > MAX_SIZE) { + ComponentGlobal_NotifikasiPeringatan( + PemberitahuanMaksimalFile + ); + onSetImgBG(null); + + return; + } + + if (backgroundProfileId != "") { + const deleteFotoBg = await funGlobal_DeleteFileById({ + fileId: backgroundProfileId, + dirId: DIRECTORY_ID.profile_background, + }); + + if (!deleteFotoBg.success) { + clientLogger.error( + "Client failed delete background:" + + deleteFotoBg.message + ); + return; + } + + if (deleteFotoBg.success) { + onSetBackgroundProfileId(""); + onSetImgBG(null); + + const uploadBackground = await funGlobal_UploadToStorage({ + file: files, + dirId: DIRECTORY_ID.profile_background, + }); + + if (!uploadBackground.success) { + clientLogger.error( + "Client failed upload background:" + + uploadBackground.message + ); + return; + } + + if (uploadBackground.success) { + onSetBackgroundProfileId(uploadBackground.data.id); + onSetImgBG(buffer); + } else { + ComponentGlobal_NotifikasiPeringatan( + "Gagal upload background profile" + ); + } + } + } else { + const uploadBackground = await funGlobal_UploadToStorage({ + file: files, + dirId: DIRECTORY_ID.profile_background, + }); + + if (uploadBackground.success) { + onSetBackgroundProfileId(uploadBackground.data.id); + onSetImgBG(buffer); + } else { + ComponentGlobal_NotifikasiPeringatan( + "Gagal upload background profile" + ); + } + } + } catch (error) { + clientLogger.error("Client error upload background:", error); + } finally { + setLoading(false); + } + }} + accept="image/png,image/jpeg" + > + {(props) => ( + + )} + +
+
+
+ + ); +} diff --git a/src/app_modules/katalog/profile/create/view_upload_foto.tsx b/src/app_modules/katalog/profile/create/view_upload_foto.tsx new file mode 100644 index 00000000..279f1282 --- /dev/null +++ b/src/app_modules/katalog/profile/create/view_upload_foto.tsx @@ -0,0 +1,216 @@ +import { DIRECTORY_ID } from "@/app/lib"; +import { MainColor } from "@/app_modules/_global/color"; +import { ComponentGlobal_BoxInformation } from "@/app_modules/_global/component"; +import { + funGlobal_DeleteFileById, + funGlobal_UploadToStorage, +} from "@/app_modules/_global/fun"; +import { MAX_SIZE } from "@/app_modules/_global/lib"; +import { PemberitahuanMaksimalFile } from "@/app_modules/_global/lib/max_size"; +import { ComponentGlobal_NotifikasiPeringatan } from "@/app_modules/_global/notif_global"; +import { clientLogger } from "@/util/clientLogger"; +import { + Avatar, + Box, + Button, + Center, + FileButton, + Loader, + Paper, + Stack, +} from "@mantine/core"; +import { IconCamera } from "@tabler/icons-react"; +import { useState } from "react"; + +export default function Profile_ViewUploadFoto({ + imgPP, + onSetImgPP, + fotoProfileId, + onSetFotoProfileId, +}: { + imgPP: string | null | undefined; + onSetImgPP: (img: string | null) => void; + fotoProfileId: string; + onSetFotoProfileId: (id: string) => void; +}) { + const [isLoading, setLoading] = useState(false); + + return ( + <> + + + +
+ {isLoading ? ( + + +
+ +
+
+
+ ) : imgPP != undefined || imgPP != null ? ( + + + + ) : ( + + + + )} +
+ +
+ { + try { + const buffer = URL.createObjectURL( + new Blob([new Uint8Array(await files.arrayBuffer())]) + ); + + if (files.size > MAX_SIZE) { + ComponentGlobal_NotifikasiPeringatan( + PemberitahuanMaksimalFile + ); + onSetImgPP(null); + + return; + } + + if (fotoProfileId != "") { + try { + setLoading(true); + const deleteFotoProfile = await funGlobal_DeleteFileById({ + fileId: fotoProfileId, + dirId: DIRECTORY_ID.profile_foto, + }); + + if (!deleteFotoProfile.success) { + clientLogger.error( + "Client failed delete photo profile:" + + deleteFotoProfile.message + ); + + return; + } + + if (deleteFotoProfile.success) { + onSetFotoProfileId(""); + onSetImgPP(null); + + const uploadPhoto = await funGlobal_UploadToStorage({ + file: files, + dirId: DIRECTORY_ID.profile_foto, + }); + + if (!uploadPhoto.success) { + clientLogger.error( + "Client failed upload photo profile::" + + uploadPhoto.message + ); + return; + } + + if (uploadPhoto.success) { + clientLogger.info( + "Client success upload foto profile" + ); + onSetFotoProfileId(uploadPhoto.data.id); + onSetImgPP(buffer); + } else { + clientLogger.error( + "Client failed upload foto:", + uploadPhoto.message + ); + ComponentGlobal_NotifikasiPeringatan( + "Gagal upload foto profile" + ); + } + } + } catch (error) { + clientLogger.error("Client error upload foto:", error); + } finally { + setLoading(false); + } + } else { + try { + setLoading(true); + const uploadPhoto = await funGlobal_UploadToStorage({ + file: files, + dirId: DIRECTORY_ID.profile_foto, + }); + + if (uploadPhoto.success) { + clientLogger.info("Client success upload foto profile"); + onSetFotoProfileId(uploadPhoto.data.id); + onSetImgPP(buffer); + } else { + clientLogger.error( + "Client failed upload foto:", + uploadPhoto.message + ); + ComponentGlobal_NotifikasiPeringatan( + "Gagal upload foto profile" + ); + } + } catch (error) { + clientLogger.error("Client error upload foto:", error); + } finally { + setLoading(false); + } + } + } catch (error) { + clientLogger.error("Client error upload foto:", error); + } + }} + accept="image/png,image/jpeg" + > + {(props) => ( + + )} + +
+
+
+ + ); +} diff --git a/src/app_modules/katalog/profile/fun/fun_create_profile.ts b/src/app_modules/katalog/profile/fun/fun_create_profile.ts index ff8eb96d..35af0660 100644 --- a/src/app_modules/katalog/profile/fun/fun_create_profile.ts +++ b/src/app_modules/katalog/profile/fun/fun_create_profile.ts @@ -3,6 +3,7 @@ import prisma from "@/app/lib/prisma"; import { RouterHome } from "@/app/lib/router_hipmi/router_home"; import { funGetUserIdByToken } from "@/app_modules/_global/fun/get"; +import backendLogger from "@/util/backendLogger"; import { Prisma } from "@prisma/client"; import { revalidatePath } from "next/cache"; @@ -19,6 +20,7 @@ export default async function funCreateNewProfile({ const userLoginId = await funGetUserIdByToken(); if (!userLoginId) { + backendLogger.error("User tidak terautentikasi"); return { status: 400, message: "User tidak terautentikasi" }; // Validasi user login } @@ -45,6 +47,7 @@ export default async function funCreateNewProfile({ }); if (!createProfile) { + backendLogger.error("Gagal membuat profile"); return { status: 400, message: "Gagal membuat profile" }; } @@ -61,7 +64,7 @@ export default async function funCreateNewProfile({ message: "Berhasil", }; } catch (error) { - console.error("Error creating profile:", error); + backendLogger.error("Terjadi kesalahan pada server", error); return { status: 500, message: "Terjadi kesalahan pada server" }; } } diff --git a/src/app_modules/zCoba/ui_coba_upload_file.tsx b/src/app_modules/zCoba/ui_coba_upload_file.tsx index 091a09fa..eb6eb30e 100644 --- a/src/app_modules/zCoba/ui_coba_upload_file.tsx +++ b/src/app_modules/zCoba/ui_coba_upload_file.tsx @@ -189,7 +189,7 @@ async function coba_ButtonFileUpload({ formData.append("dirId", dirId); try { - const res = await fetch("https://wibu-storage.wibudev.com/api/upload", { + const res = await fetch("https://wibu-storage.wibudev.com/api/image/upload", { method: "POST", body: formData, headers: { diff --git a/src/middleware.ts b/src/middleware.ts index 705192f2..6a72d076 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -19,7 +19,8 @@ const middlewareConfig: MiddlewareConfig = { userPath: "/dev/home", publicRoutes: [ "/", - "/api/upload", + "/api/logs/*", + "/api/image/*", "/api/job/*", "/api/validation", "/api/auth/*", diff --git a/src/util/backendLogger.ts b/src/util/backendLogger.ts new file mode 100644 index 00000000..9a8e3d30 --- /dev/null +++ b/src/util/backendLogger.ts @@ -0,0 +1,37 @@ +// src/utils/backendLogger.ts +import winston from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; +import path from "path"; + +const backendLogger = winston.createLogger({ + level: "info", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + // Error logs + new DailyRotateFile({ + filename: path.join("logs/backend/error-%DATE%.log"), + datePattern: "YYYY-MM-DD", + level: "error", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + }), + // Combined logs + new DailyRotateFile({ + filename: path.join("logs/backend/combined-%DATE%.log"), + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + }), + // Console output in development + ...(process.env.NODE_ENV !== "production" + ? [new winston.transports.Console()] + : []), + ], +}); + +export default backendLogger; diff --git a/src/util/clientLogger.ts b/src/util/clientLogger.ts new file mode 100644 index 00000000..4782df23 --- /dev/null +++ b/src/util/clientLogger.ts @@ -0,0 +1,87 @@ +// src/utils/clientLogger.ts +interface LogEntry { + level: "info" | "warn" | "error"; + message: string; + data?: any; + timestamp?: string; +} + +class ClientLogger { + private queue: LogEntry[] = []; + private readonly maxQueueSize: number = 10; + private readonly apiEndpoint: string = "/api/logs"; + private isSending: boolean = false; + + private async sendLogs(): Promise { + if (this.isSending || this.queue.length === 0) return; + + this.isSending = true; + const logsToSend = [...this.queue]; + this.queue = []; + + try { + const response = await fetch(this.apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(logsToSend), + }); + + if (!response.ok) { + console.error("Failed to send logs:", response.statusText); + // Restore logs to queue if send failed + this.queue = [...logsToSend, ...this.queue]; + } + } catch (error) { + console.error("Error sending logs:", error); + // Restore logs to queue if send failed + this.queue = [...logsToSend, ...this.queue]; + } finally { + this.isSending = false; + } + } + + private addToQueue(entry: LogEntry): void { + this.queue.push({ + ...entry, + timestamp: new Date().toISOString(), + }); + + if (this.queue.length >= this.maxQueueSize) { + this.sendLogs(); + } + } + + public info(message: string, data?: any): void { + this.addToQueue({ level: "info", message, data }); + } + + public warn(message: string, data?: any): void { + this.addToQueue({ level: "warn", message, data }); + // Send immediately for warnings + this.sendLogs(); + } + + public error(message: string, error?: Error | any): void { + const errorData = + error instanceof Error + ? { + name: error.name, + message: error.message, + stack: error.stack, + } + : error; + + this.addToQueue({ level: "error", message, data: errorData }); + // Send immediately for errors + this.sendLogs(); + } + + // Flush remaining logs (useful when page is about to unload) + public flush(): void { + this.sendLogs(); + } +} + +export const clientLogger = new ClientLogger(); diff --git a/src/util/frontend-logger.ts b/src/util/frontend-logger.ts new file mode 100644 index 00000000..6eff4277 --- /dev/null +++ b/src/util/frontend-logger.ts @@ -0,0 +1,101 @@ +// utils/frontend-logger.ts +import winston from "winston"; +import DailyRotateFile from "winston-daily-rotate-file"; +import path from "path"; + +// Define log levels and their priorities +const levels = { + error: 0, + warn: 1, + info: 2, + debug: 3, +}; + +// Define interface for log entries +export interface LogEntry { + level: keyof typeof levels; + message: string; + data?: any; + timestamp?: string; + userAgent?: string; + ip?: string; + url?: string; +} + +// Custom format for log entries +const logFormat = winston.format.combine( + winston.format.timestamp({ + format: "YYYY-MM-DD HH:mm:ss", + }), + winston.format.metadata({ fillExcept: ["message", "level", "timestamp"] }), + winston.format.json() +); + +// Create the logger instance +const frontendLogger: winston.Logger = winston.createLogger({ + levels, + level: process.env.LOG_LEVEL || "info", + format: logFormat, + transports: [ + // Daily Rotate File for errors + new DailyRotateFile({ + filename: path.join(process.cwd(), "logs/frontend/error-%DATE%.log"), + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + level: "error", + format: logFormat, + }), + + // Daily Rotate File for all logs + new DailyRotateFile({ + filename: path.join(process.cwd(), "logs/frontend/combined-%DATE%.log"), + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + format: logFormat, + }), + ], + // Handle errors from the logger itself + exitOnError: false, +}); + +// Add console transport in development +if (process.env.NODE_ENV !== "production") { + frontendLogger.add( + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + }) + ); +} + +// Helper functions for type-safe logging +export function logError(message: string, data?: any) { + frontendLogger.error(message, { data }); +} + +export function logWarn(message: string, data?: any) { + frontendLogger.warn(message, { data }); +} + +export function logInfo(message: string, data?: any) { + frontendLogger.info(message, { data }); +} + +export function logDebug(message: string, data?: any) { + frontendLogger.debug(message, { data }); +} + +// Helper function for dynamic logging +export function log(entry: LogEntry) { + const { level, message, ...metadata } = entry; + frontendLogger[level](message, metadata); +} + +// Export the logger instance as default +export default frontendLogger;