From c0d7ec351761cbe387998784e20f630e3734b136 Mon Sep 17 00:00:00 2001 From: mbaharip <62494292+mbahArip@users.noreply.github.com> Date: Sat, 20 May 2023 23:39:43 +0700 Subject: [PATCH] Rewrite validate path and re-organizing utils and type --- src/app/(api)/api/banner/route.ts | 2 +- .../(api)/api/files/[encryptedId]/route.ts | 126 ++++----- src/app/(api)/api/files/route.ts | 112 ++++---- src/app/(api)/api/validate/[...path]/route.ts | 227 +++++++++++++++++ src/app/(api)/api/validatePath/route.ts | 241 ------------------ src/config/api.config.js | 3 +- src/types/api/files.ts | 12 + src/types/api/path.ts | 7 + src/types/general/constant.ts | 8 + src/types/general/index.ts | 8 + src/utils/apiHelper/createErrorPayload.ts | 32 +++ .../{driveClient.ts => apiHelper/gdrive.ts} | 19 +- src/utils/{ => apiHelper}/getSearchParams.ts | 0 src/utils/axiosHelper.ts | 44 ---- src/utils/driveHelper.ts | 37 --- src/utils/encryptionHelper/passwordHash.ts | 16 ++ src/utils/generalHelper/fetch.ts | 50 ++++ 17 files changed, 487 insertions(+), 457 deletions(-) create mode 100644 src/app/(api)/api/validate/[...path]/route.ts delete mode 100644 src/app/(api)/api/validatePath/route.ts create mode 100644 src/types/api/files.ts create mode 100644 src/types/api/path.ts create mode 100644 src/types/general/constant.ts create mode 100644 src/types/general/index.ts create mode 100644 src/utils/apiHelper/createErrorPayload.ts rename src/utils/{driveClient.ts => apiHelper/gdrive.ts} (62%) rename src/utils/{ => apiHelper}/getSearchParams.ts (100%) delete mode 100644 src/utils/axiosHelper.ts delete mode 100644 src/utils/driveHelper.ts create mode 100644 src/utils/encryptionHelper/passwordHash.ts create mode 100644 src/utils/generalHelper/fetch.ts diff --git a/src/app/(api)/api/banner/route.ts b/src/app/(api)/api/banner/route.ts index 8e70b86..c276d77 100644 --- a/src/app/(api)/api/banner/route.ts +++ b/src/app/(api)/api/banner/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { ErrorResponse } from "types/googleapis"; -import getSearchParams from "utils/getSearchParams"; +import getSearchParams from "utils/apiHelper/getSearchParams"; import { ExtendedError } from "utils/driveHelper"; import driveClient from "utils/driveClient"; import { shortDecrypt } from "utils/encryptionHelper"; diff --git a/src/app/(api)/api/files/[encryptedId]/route.ts b/src/app/(api)/api/files/[encryptedId]/route.ts index 1ffc0c1..e3e2095 100644 --- a/src/app/(api)/api/files/[encryptedId]/route.ts +++ b/src/app/(api)/api/files/[encryptedId]/route.ts @@ -1,20 +1,17 @@ +import shortEncryption from "utils/encryptionHelper/shortEncryption"; +import gdrive from "utils/apiHelper/gdrive"; +import { drive_v3 } from "googleapis"; +import apiConfig from "config/api.config"; +import { API_Response } from "types/api"; import { - ErrorResponse, FileResponse, FilesResponse, -} from "types/googleapis"; +} from "types/api/files"; import { NextRequest, NextResponse } from "next/server"; -import getSearchParams from "utils/getSearchParams"; -import { - shortDecrypt, - shortEncrypt, -} from "utils/encryptionHelper"; -import driveClient from "utils/driveClient"; -import apiConfig from "config/api.config"; -import { - ExtendedError, - hiddenFiles, -} from "utils/driveHelper"; +import createErrorPayload from "utils/apiHelper/createErrorPayload"; +import ExtendedError from "utils/generalHelper/extendedError"; +import getSearchParams from "utils/apiHelper/getSearchParams"; +import { Constant } from "types/general/constant"; export async function GET( request: NextRequest, @@ -25,7 +22,7 @@ export async function GET( try { const { pageToken, banner, thumbnail } = getSearchParams(request.url, ["pageToken", "banner"]); - const id = shortDecrypt(params.encryptedId); + const id = shortEncryption.decrypt(params.encryptedId); if (id === apiConfig.files.rootFolder) { return NextResponse.redirect( `${apiConfig.basePath}/api/files`, @@ -35,7 +32,7 @@ export async function GET( ); } - const file = await driveClient.files.get({ + const file = await gdrive.files.get({ fileId: id, fields: apiConfig.files.field, }); @@ -44,7 +41,12 @@ export async function GET( const msg = file.data.trashed ? "File has been deleted" : "File not found"; - throw new ExtendedError(msg, 404, "notFound"); + throw new ExtendedError( + Constant.apiFileNotFound, + 404, + "notFound", + msg, + ); } if ( @@ -60,15 +62,17 @@ export async function GET( ); } - const payload: FileResponse = { + const payload: API_Response = { success: true, timestamp: new Date().toISOString(), responseTime: Date.now() - _start, - file: { + data: { ...file.data, - id: shortEncrypt(file.data.id as string), + id: shortEncryption.encrypt( + file.data.id as string, + ), webContentLink: - shortEncrypt( + shortEncryption.encrypt( file.data.webContentLink as string, ) || undefined, }, @@ -77,7 +81,7 @@ export async function GET( return NextResponse.json(payload, { status: 200, headers: { - "Cache-Control": apiConfig.cache, + "Cache-Control": apiConfig.cacheControl, }, }); } @@ -87,7 +91,7 @@ export async function GET( "trashed = false", "'me' in owners", ]; - const folderContents = await driveClient.files.list({ + const fetchFolderContents = await gdrive.files.list({ q: `${query.join(" and ")}`, fields: `files(${apiConfig.files.field}), nextPageToken`, orderBy: apiConfig.files.orderBy, @@ -95,11 +99,11 @@ export async function GET( pageToken: pageToken || undefined, }); - const readmeFile = folderContents.data.files?.find( + const readmeFile = fetchFolderContents.data.files?.find( (file) => file.name === apiConfig.files.specialFile.readme, ); - const bannerFile = folderContents.data.files?.find( + const bannerFile = fetchFolderContents.data.files?.find( (file) => file.name?.startsWith( apiConfig.files.specialFile.banner, @@ -109,9 +113,10 @@ export async function GET( if (banner === "1") { if (!bannerFile) { throw new ExtendedError( - "Banner not found.", + Constant.apiFileNotFound, 404, "notFound", + "The banner file is not found.", ); } if ( @@ -125,7 +130,9 @@ export async function GET( } return NextResponse.redirect( - `${apiConfig.basePath}/api/banner?id=${shortEncrypt( + `${ + apiConfig.basePath + }/api/banner?id=${shortEncryption.encrypt( bannerFile.id as string, )}`, { @@ -135,7 +142,7 @@ export async function GET( } const folderList = - folderContents.data.files + (fetchFolderContents.data.files ?.filter( (file) => file.mimeType === @@ -143,61 +150,56 @@ export async function GET( ) .map((file) => ({ ...file, - id: shortEncrypt(file.id as string), - })) || []; + id: shortEncryption.encrypt(file.id as string), + })) as drive_v3.Schema$File[]) || []; const fileList = - folderContents.data.files + (fetchFolderContents.data.files ?.filter( (file) => - file.mimeType !== - "application/vnd.google-apps.folder" && - !hiddenFiles.some((hiddenFile) => - file.name?.startsWith(hiddenFile), + !file.mimeType?.startsWith( + "application/vnd.google-apps", + ) && + !apiConfig.files.hiddenFiles.some( + (hiddenFile) => + file.name?.startsWith(hiddenFile), ), ) .map((file) => ({ ...file, - id: shortEncrypt(file.id as string), + id: shortEncryption.encrypt(file.id as string), webContentLink: - shortEncrypt(file.webContentLink as string) || - undefined, - })) || []; + shortEncryption.encrypt( + file.webContentLink as string, + ) || undefined, + })) as drive_v3.Schema$File[]) || []; - const payload: FilesResponse = { + const payload: API_Response = { success: true, timestamp: new Date().toISOString(), responseTime: Date.now() - _start, - folders: folderList, - files: fileList, - isReadmeExists: !!readmeFile, - isBannerExists: !!bannerFile, - nextPageToken: - folderContents.data.nextPageToken || undefined, + data: { + folders: folderList, + files: fileList, + isReadmeExists: !!readmeFile, + isBannerExists: !!bannerFile, + nextPageToken: + fetchFolderContents.data.nextPageToken || + undefined, + }, }; return NextResponse.json(payload, { status: 200, headers: { - "Cache-Control": apiConfig.cache, + "Cache-Control": apiConfig.cacheControl, }, }); } catch (error: any) { - const payload: ErrorResponse = { - success: false, - timestamp: new Date().toISOString(), - responseTime: Date.now() - _start, - code: error.code || 500, - errors: { - message: - error.errors?.[0].message || - error.message || - "Unknown error", - reason: - error.errors?.[0].reason || - error.cause || - "internalError", - }, - }; + const payload = createErrorPayload( + error, + "GET /api/files", + _start, + ); return NextResponse.json(payload, { status: payload.code || 500, diff --git a/src/app/(api)/api/files/route.ts b/src/app/(api)/api/files/route.ts index 5a3dfaa..a9fd7ac 100644 --- a/src/app/(api)/api/files/route.ts +++ b/src/app/(api)/api/files/route.ts @@ -1,19 +1,20 @@ import { NextRequest, NextResponse } from "next/server"; +import { drive_v3 } from "googleapis"; import apiConfig from "config/api.config"; -import getSearchParams from "utils/getSearchParams"; -import { - ErrorResponse, - FilesResponse, -} from "types/googleapis"; -import driveClient from "utils/driveClient"; -import { - ExtendedError, - hiddenFiles, -} from "utils/driveHelper"; -import { shortEncrypt } from "utils/encryptionHelper"; + +import gdrive from "utils/apiHelper/gdrive"; +import createErrorPayload from "utils/apiHelper/createErrorPayload"; +import getSearchParams from "utils/apiHelper/getSearchParams"; +import shortEncryption from "utils/encryptionHelper/shortEncryption"; +import ExtendedError from "utils/generalHelper/extendedError"; + +import { API_Response } from "types/api"; +import { FilesResponse } from "types/api/files"; +import { Constant } from "types/general/constant"; export async function GET(request: NextRequest) { const _start = Date.now(); + try { const { pageToken, banner } = getSearchParams( request.url, @@ -25,14 +26,13 @@ export async function GET(request: NextRequest) { "'me' in owners", `parents = '${apiConfig.files.rootFolder}'`, ]; - const fetchFolderContents = - await driveClient.files.list({ - q: `${query.join(" and ")}`, - fields: `files(${apiConfig.files.field}), nextPageToken`, - orderBy: apiConfig.files.orderBy, - pageSize: apiConfig.files.itemsPerPage, - pageToken: pageToken || undefined, - }); + const fetchFolderContents = await gdrive.files.list({ + q: `${query.join(" and ")}`, + fields: `files(${apiConfig.files.field}), nextPageToken`, + orderBy: apiConfig.files.orderBy, + pageSize: apiConfig.files.itemsPerPage, + pageToken: pageToken || undefined, + }); const readmeFile = fetchFolderContents.data.files?.find( (file) => @@ -48,9 +48,10 @@ export async function GET(request: NextRequest) { if (banner === "1") { if (!bannerFile) { throw new ExtendedError( - "Banner not found.", + Constant.apiFileNotFound, 404, "notFound", + "The banner file is not found.", ); } if ( @@ -64,7 +65,9 @@ export async function GET(request: NextRequest) { } return NextResponse.redirect( - `${apiConfig.basePath}/api/banner?id=${shortEncrypt( + `${ + apiConfig.basePath + }/api/banner?id=${shortEncryption.encrypt( bannerFile.id as string, )}`, { @@ -74,7 +77,7 @@ export async function GET(request: NextRequest) { } const folderList = - fetchFolderContents.data.files + (fetchFolderContents.data.files ?.filter( (file) => file.mimeType === @@ -82,61 +85,56 @@ export async function GET(request: NextRequest) { ) .map((file) => ({ ...file, - id: shortEncrypt(file.id as string), - })) || []; + id: shortEncryption.encrypt(file.id as string), + })) as drive_v3.Schema$File[]) || []; const fileList = - fetchFolderContents.data.files + (fetchFolderContents.data.files ?.filter( (file) => - file.mimeType !== - "application/vnd.google-apps.folder" && - !hiddenFiles.some((hiddenFile) => - file.name?.startsWith(hiddenFile), + !file.mimeType?.startsWith( + "application/vnd.google-apps", + ) && + !apiConfig.files.hiddenFiles.some( + (hiddenFile) => + file.name?.startsWith(hiddenFile), ), ) .map((file) => ({ ...file, - id: shortEncrypt(file.id as string), + id: shortEncryption.encrypt(file.id as string), webContentLink: - shortEncrypt(file.webContentLink as string) || - undefined, - })) || []; + shortEncryption.encrypt( + file.webContentLink as string, + ) || undefined, + })) as drive_v3.Schema$File[]) || []; - const payload: FilesResponse = { + const payload: API_Response = { success: true, timestamp: new Date().toISOString(), responseTime: Date.now() - _start, - folders: folderList, - files: fileList, - isReadmeExists: !!readmeFile, - isBannerExists: !!bannerFile, - nextPageToken: - fetchFolderContents.data.nextPageToken || undefined, + data: { + folders: folderList, + files: fileList, + isReadmeExists: !!readmeFile, + isBannerExists: !!bannerFile, + nextPageToken: + fetchFolderContents.data.nextPageToken || + undefined, + }, }; return NextResponse.json(payload, { status: 200, headers: { - "Cache-Control": apiConfig.cache, + "Cache-Control": apiConfig.cacheControl, }, }); } catch (error: any) { - const payload: ErrorResponse = { - success: false, - timestamp: new Date().toISOString(), - responseTime: Date.now() - _start, - code: error.code || 500, - errors: { - message: - error.errors?.[0].message || - error.message || - "Unknown error", - reason: - error.errors?.[0].reason || - error.cause || - "internalError", - }, - }; + const payload = createErrorPayload( + error, + "GET /api/files", + _start, + ); return NextResponse.json(payload, { status: payload.code || 500, diff --git a/src/app/(api)/api/validate/[...path]/route.ts b/src/app/(api)/api/validate/[...path]/route.ts new file mode 100644 index 0000000..4f0db36 --- /dev/null +++ b/src/app/(api)/api/validate/[...path]/route.ts @@ -0,0 +1,227 @@ +import { NextRequest, NextResponse } from "next/server"; +import createErrorPayload from "utils/apiHelper/createErrorPayload"; +import { RequestContext } from "types/general"; +import apiConfig from "config/api.config"; +import gdrive from "utils/apiHelper/gdrive"; +import ExtendedError from "utils/generalHelper/extendedError"; +import { Constant } from "types/general/constant"; +import { + FilePath, + ValidateFilePathResponse, +} from "types/api/path"; +import shortEncryption from "utils/encryptionHelper/shortEncryption"; +import { cookies } from "next/headers"; +import passwordHash from "utils/encryptionHelper/passwordHash"; +import * as console from "console"; +import { API_Response } from "types/api"; + +export async function GET( + request: NextRequest, + { params }: RequestContext<"path", string[]>, +) { + const _start = Date.now(); + const pathArray = params.path; + + try { + const fetchRootId = + apiConfig.files.rootFolder !== "root" + ? apiConfig.files.rootFolder + : gdrive.files.get({ + fileId: "root", + fields: "id", + }); + const fetchPathId = pathArray.map(async (path) => { + const query = [ + "trashed = false", + "'me' in owners", + `name = '${path}'`, + ]; + const fetchFolderContents = await gdrive.files.list({ + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }); + + if (!fetchFolderContents.data.files?.length) { + throw new ExtendedError( + Constant.apiFileNotFound, + 404, + "notFound", + `Path "${path}" is not found`, + ); + } + + return { + path, + data: fetchFolderContents.data.files.map( + (file) => ({ + id: file.id, + parents: file.parents?.[0], + mimeType: file.mimeType, + }), + ), + }; + }); + + const [rootId, pathId] = await Promise.all([ + fetchRootId, + Promise.all(fetchPathId), + ]); + + const selectedPath: string[] = []; + const mappedPath: FilePath[] = []; + + // Check path validity + pathId.forEach((path, index) => { + let checkPath; + if (index === 0) { + checkPath = path.data.find( + (file) => file.parents === rootId, + ); + } else { + checkPath = path.data.find( + (file) => + file.parents === selectedPath[index - 1], + ); + } + if (!checkPath) { + throw new ExtendedError( + Constant.apiFileNotFound, + 404, + "notFound", + `Path "${path.path}" is not found`, + ); + } + selectedPath.push(checkPath.id as string); + mappedPath.push({ + name: path.path, + encryptedId: shortEncryption.encrypt( + checkPath.id as string, + ), + mimeType: checkPath.mimeType as string, + }); + }); + + // Check for protected folder + const fetchProtectedFolder = mappedPath.map( + async (path) => { + if ( + path.mimeType !== + "application/vnd.google-apps.folder" + ) + return; + + const query = [ + "trashed = false", + "'me' in owners", + `'${shortEncryption.decrypt( + path.encryptedId, + )}' in parents`, + `name = '${apiConfig.files.specialFile.password}'`, + ]; + + const fetchFolderContents = await gdrive.files.list( + { + q: `${query.join(" and ")}`, + fields: "files(id, name, mimeType, parents)", + }, + ); + + if (!fetchFolderContents.data.files?.length) return; + + return { + path: path.name, + passwordId: fetchFolderContents.data.files[0].id, + }; + }, + ); + + let protectedFolder = await Promise.all( + fetchProtectedFolder, + ); + protectedFolder = protectedFolder.filter( + (folder) => folder !== undefined, + ); + const nearestProtectedFolder = + protectedFolder[protectedFolder.length - 1] ?? null; + + console.log(nearestProtectedFolder); + + if ( + nearestProtectedFolder && + nearestProtectedFolder.passwordId + ) { + const fetchPassword = await gdrive.files.get( + { + fileId: nearestProtectedFolder.passwordId, + alt: "media", + }, + { responseType: "text" }, + ); + + const userPassword = cookies().get( + `next-gdrive-password`, + )?.value; + if (!userPassword) { + throw new ExtendedError( + Constant.apiNotAuthorized, + 401, + "unauthorized", + `You need to provide password to access this folder / file`, + ); + } + + const parsedUserPassword = JSON.parse(userPassword); + const nearestFolderPassword = + parsedUserPassword[ + decodeURIComponent(nearestProtectedFolder.path) + ]; + if (!nearestFolderPassword) { + throw new ExtendedError( + Constant.apiNotAuthorized, + 401, + "unauthorized", + `You need to provide password to access this folder / file`, + ); + } + + if ( + !passwordHash.verify( + fetchPassword.data as string, + nearestFolderPassword, + ) + ) { + throw new ExtendedError( + Constant.apiNotAuthorized, + 401, + "unauthorized", + `The password you provided is incorrect`, + ); + } + } + + const payload: API_Response = + { + success: true, + timestamp: new Date().toISOString(), + responseTime: Date.now() - _start, + data: mappedPath, + }; + + return NextResponse.json(payload, { + status: 200, + headers: { + "Cache-Control": apiConfig.cacheControl, + }, + }); + } catch (error: any) { + const payload = createErrorPayload( + error, + `GET /api/verify/${pathArray.join("/")}`, + _start, + ); + + return NextResponse.json(payload, { + status: payload.code || 500, + }); + } +} diff --git a/src/app/(api)/api/validatePath/route.ts b/src/app/(api)/api/validatePath/route.ts deleted file mode 100644 index 8b770b2..0000000 --- a/src/app/(api)/api/validatePath/route.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - ErrorResponse, - TPath, - ValidatePathResponse, -} from "types/googleapis"; -import { ExtendedError } from "utils/driveHelper"; -import apiConfig from "config/api.config"; -import driveClient from "utils/driveClient"; -import { - shortDecrypt, - shortEncrypt, -} from "utils/encryptionHelper"; -import { cookies } from "next/headers"; - -export async function GET(request: NextRequest) { - const _start = Date.now(); - try { - const path = new URL(request.url).searchParams.get( - "path", - ); - if (!path) { - throw new ExtendedError( - "No path provided", - 400, - "badRequest", - ); - } - - let pathArray: string[] = path.split(/[\\/]/); - pathArray = pathArray.filter((path) => path !== ""); - - const getRootId = - apiConfig.files.rootFolder !== "root" - ? apiConfig.files.rootFolder - : driveClient.files - .get({ - fileId: "root", - fields: "id", - }) - .then((res) => res.data.id); - const getPathId = pathArray.map(async (path, index) => { - const query = [ - "trashed = false", - "'me' in owners", - `name = '${path}'`, - ]; - const fetchFolderContents = - await driveClient.files.list({ - q: `${query.join(" and ")}`, - fields: "files(id, name, mimeType, parents)", - }); - - if (!fetchFolderContents.data.files?.length) { - throw new ExtendedError( - `Path ${path} is not found`, - 404, - "notFound", - ); - } - - return { - path, - data: fetchFolderContents.data.files.map( - (file) => ({ - id: file.id, - parents: file.parents?.[0], - mimeType: file.mimeType, - }), - ), - }; - }); - - const [rootId, pathId] = await Promise.all([ - getRootId, - Promise.all(getPathId), - ]); - - const selectedPath: string[] = []; - const mapIds: TPath[] = []; - - // Check path validity - pathId.forEach((path, index) => { - if (index === 0) { - // Check if root folder inside data id - const checkRoot = path.data.find( - (item) => item.parents === rootId, - ); - if (!checkRoot) { - throw new ExtendedError( - `Path "${path.path}" is either not found or not valid`, - 400, - "badRequest", - ); - } - - selectedPath.push(checkRoot.id as string); - mapIds.push({ - name: path.path, - id: shortEncrypt(checkRoot.id as string), - mimeType: checkRoot.mimeType as string, - }); - } else { - const checkPath = path.data.find( - (item) => - item.parents === selectedPath[index - 1], - ); - if (!checkPath) { - throw new ExtendedError( - `Path "${path.path}" is either not found or not valid`, - 400, - "badRequest", - ); - } - - selectedPath.push(checkPath.id as string); - mapIds.push({ - name: path.path, - id: shortEncrypt(checkPath.id as string), - mimeType: checkPath.mimeType as string, - }); - } - }); - - // Check for protected folder - const getPassword = mapIds.map(async (path) => { - if ( - path.mimeType !== - "application/vnd.google-apps.folder" - ) - return; - - const query = [ - "trashed = false", - "'me' in owners", - `'${shortDecrypt(path.id)}' in parents`, - `name = '${apiConfig.files.specialFile.password}'`, - ]; - - const folderContents = await driveClient.files.list({ - q: `${query.join(" and ")}`, - fields: "files(id, name, mimeType, parents)", - }); - - if (!folderContents.data.files?.length) return; - - return { - path: path.name, - password: folderContents.data.files?.[0].id, - }; - }); - - let password = await Promise.all(getPassword); - password = password.filter( - (item) => item !== undefined, - ); - const lastProtectedFolder = - password[password.length - 1]; - - // Check password - if (lastProtectedFolder?.password) { - const fetchPassword = await driveClient.files.get( - { - fileId: lastProtectedFolder.password, - alt: "media", - }, - { responseType: "text" }, - ); - - const userPassword = cookies().get( - `next-gdrive-password/${encodeURIComponent( - lastProtectedFolder.path, - )}`, - )?.value; - console.log( - `next-gdrive-password/${encodeURIComponent( - lastProtectedFolder.path, - )}`, - shortEncrypt("loremipsum"), - ); - if (!userPassword) { - throw new ExtendedError( - `You are not authorized to access this folder`, - 401, - "unauthorized", - ); - } - console.log( - shortDecrypt(userPassword), - fetchPassword.data, - ); - if ( - shortDecrypt(userPassword) !== fetchPassword.data - ) { - throw new ExtendedError( - `The password you entered is incorrect`, - 401, - "unauthorized", - ); - } - } - - const payload: ValidatePathResponse = { - success: true, - timestamp: new Date().toISOString(), - responseTime: Date.now() - _start, - data: mapIds, - password: lastProtectedFolder?.password - ? shortEncrypt(lastProtectedFolder.password) - : undefined, - }; - - return NextResponse.json(payload, { - status: 200, - headers: { - "Cache-Control": apiConfig.cache, - }, - }); - } catch (error: any) { - const payload: ErrorResponse = { - success: false, - timestamp: new Date().toISOString(), - responseTime: Date.now() - _start, - code: error.code || 500, - errors: { - message: - error.errors?.[0].message || - error.message || - "Unknown error", - reason: - error.errors?.[0].reason || - error.cause || - "internalError", - }, - }; - - return NextResponse.json(payload, { - status: payload.code || 500, - }); - } -} diff --git a/src/config/api.config.js b/src/config/api.config.js index f019bba..60dd16f 100644 --- a/src/config/api.config.js +++ b/src/config/api.config.js @@ -68,6 +68,7 @@ module.exports = { */ banner: ".banner", }, + hiddenFiles: [".password", ".readme.md", ".banner"], itemsPerPage: 50, searchResult: 10, /** @@ -108,5 +109,5 @@ module.exports = { /** * https://web.dev/uses-long-cache-ttl/ */ - cache: "s-maxage=60, stale-while-revalidate", + cacheControl: "s-maxage=60, stale-while-revalidate", }; diff --git a/src/types/api/files.ts b/src/types/api/files.ts new file mode 100644 index 0000000..de22bbd --- /dev/null +++ b/src/types/api/files.ts @@ -0,0 +1,12 @@ +import { drive_v3 } from "googleapis"; + +export type FilesResponse = { + folders: drive_v3.Schema$File[]; + files: drive_v3.Schema$File[]; + isReadmeExists?: boolean; + isBannerExists?: boolean; + nextPageToken?: string; +}; + +export type FileResponse = drive_v3.Schema$File; +export type SearchResponse = drive_v3.Schema$File[]; diff --git a/src/types/api/path.ts b/src/types/api/path.ts new file mode 100644 index 0000000..d1026fe --- /dev/null +++ b/src/types/api/path.ts @@ -0,0 +1,7 @@ +export type FilePath = { + name: string; + encryptedId: string; + mimeType: string; +}; + +export type ValidateFilePathResponse = FilePath[]; diff --git a/src/types/general/constant.ts b/src/types/general/constant.ts new file mode 100644 index 0000000..5331bd9 --- /dev/null +++ b/src/types/general/constant.ts @@ -0,0 +1,8 @@ +export enum Constant { + // API + apiFileNotFound = "File not found", + apiNotAuthorized = "You are not authorized to access this resource", + apiNoResponse = "No response from server", + apiBadRequest = "Bad request", + apiInternalError = "Internal server error", +} diff --git a/src/types/general/index.ts b/src/types/general/index.ts new file mode 100644 index 0000000..898ac01 --- /dev/null +++ b/src/types/general/index.ts @@ -0,0 +1,8 @@ +export type RequestContext< + N extends string, + T = unknown, +> = { + params: { + [K in N]: T; + }; +}; diff --git a/src/utils/apiHelper/createErrorPayload.ts b/src/utils/apiHelper/createErrorPayload.ts new file mode 100644 index 0000000..f521686 --- /dev/null +++ b/src/utils/apiHelper/createErrorPayload.ts @@ -0,0 +1,32 @@ +import { API_Error } from "types/api"; +import { Constant } from "types/general/constant"; +import ExtendedError from "utils/generalHelper/extendedError"; + +function createErrorPayload( + error: any, + path: string, + requestStart: number = Date.now(), +): API_Error { + console.error(`Error @${path}: `, error.message); + + const payload: API_Error = { + success: false, + timestamp: new Date().toISOString(), + responseTime: Date.now() - requestStart, + code: error.code || 500, + message: Constant.apiInternalError, + category: "internalError", + reason: error.message || Constant.apiInternalError, + }; + + if (error instanceof ExtendedError) { + payload.message = error.message; + payload.category = error.category || "internalError"; + payload.reason = + error.reason || Constant.apiInternalError; + } + + return payload; +} + +export default createErrorPayload; diff --git a/src/utils/driveClient.ts b/src/utils/apiHelper/gdrive.ts similarity index 62% rename from src/utils/driveClient.ts rename to src/utils/apiHelper/gdrive.ts index 940b8df..325630e 100644 --- a/src/utils/driveClient.ts +++ b/src/utils/apiHelper/gdrive.ts @@ -1,16 +1,7 @@ import apiConfig from "config/api.config"; import { drive_v3, google } from "googleapis"; -import { decrypt } from "utils/encryptionHelper"; - -// const decryptedSecret: string = decrypt( -// apiConfig.client_secret, -// process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string, -// ); -// const decryptedRefreshToken: string = decrypt( -// apiConfig.refresh_token, -// process.env.NEXT_PUBLIC_ENCRYPTION_KEY as string, -// ); +//TODO: Move client_secret and refresh_token to config after setup page is done const config = { client_id: process.env.NODE_ENV === "development" @@ -34,12 +25,12 @@ oauth2Client.setCredentials({ refresh_token: config.refresh_token as string, }); -let gdriveInstance; -if (!gdriveInstance) { - gdriveInstance = google.drive({ +let gdrive; +if (!gdrive) { + gdrive = google.drive({ version: "v3", auth: oauth2Client, }); } -export default gdriveInstance as drive_v3.Drive; +export default gdrive as drive_v3.Drive; diff --git a/src/utils/getSearchParams.ts b/src/utils/apiHelper/getSearchParams.ts similarity index 100% rename from src/utils/getSearchParams.ts rename to src/utils/apiHelper/getSearchParams.ts diff --git a/src/utils/axiosHelper.ts b/src/utils/axiosHelper.ts deleted file mode 100644 index 16f335a..0000000 --- a/src/utils/axiosHelper.ts +++ /dev/null @@ -1,44 +0,0 @@ -import axios, { AxiosError } from "axios"; -import apiConfig from "config/api.config"; - -const fetch = axios.create({ - baseURL: apiConfig.basePath, - maxRate: 5, - timeout: 10000, -}); - -fetch.interceptors.response.use( - (response) => response, - (error: AxiosError) => { - if (error.response) { - const payload = new ExtendedError( - error.message, - error.response.data.code, - error.response.data.category, - error.response.data.reason, - ); - // Terjadi ketika request berhasil dikirimkan, namun server memberikan response dengan status code di luar range 2xx. - return Promise.reject(JSON.stringify(payload)); - } else if (error.request) { - const payload = new ExtendedError( - Constant["noResponse"], - 500, - "noResponse", - error.message, - ); - // Terjadi ketika request dikirimkan namun tidak menerima response dari server. - return Promise.reject(JSON.stringify(payload)); - } else { - const payload = new ExtendedError( - Constant["badRequest"], - 400, - "badRequest", - error.message, - ); - // Terjadi ketika terjadi kesalahan saat melakukan request. - return Promise.reject(JSON.stringify(payload)); - } - }, -); - -export default fetch; diff --git a/src/utils/driveHelper.ts b/src/utils/driveHelper.ts deleted file mode 100644 index eb78905..0000000 --- a/src/utils/driveHelper.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { drive_v3 } from "googleapis"; -import { shortDecrypt } from "utils/encryptionHelper"; - -export const hiddenFiles = [ - ".password", - ".readme.md", - ".banner", -]; -export function createFileId( - data: drive_v3.Schema$File, - encrypted: boolean = false, -) { - if (process.env.ENCRYPTION_KEY) { - } - if (encrypted) { - return `${encodeURIComponent( - data.name as string, - )}:${shortDecrypt(data.id as string)?.slice(0, 8)}`; - } - return `${encodeURIComponent( - data.name as string, - )}:${data.id?.slice(0, 8)}`; -} - -export class ExtendedError extends Error { - code?: number; - - constructor( - message?: string, - code?: number, - reason?: string, - ) { - super(message); - this.code = code; - this.cause = reason; - } -} diff --git a/src/utils/encryptionHelper/passwordHash.ts b/src/utils/encryptionHelper/passwordHash.ts new file mode 100644 index 0000000..55d19ec --- /dev/null +++ b/src/utils/encryptionHelper/passwordHash.ts @@ -0,0 +1,16 @@ +import { createHash } from "crypto"; + +function encode(text: string): string { + return createHash("sha256").update(text).digest("hex"); +} + +function verify(text: string, hash: string): boolean { + return encode(text) === hash; +} + +const passwordHash = { + encode, + verify, +}; + +export default passwordHash; diff --git a/src/utils/generalHelper/fetch.ts b/src/utils/generalHelper/fetch.ts new file mode 100644 index 0000000..5a5fa02 --- /dev/null +++ b/src/utils/generalHelper/fetch.ts @@ -0,0 +1,50 @@ +import axios, { AxiosError } from "axios"; +import apiConfig from "config/api.config"; +import { API_Error } from "types/api"; +import ExtendedError from "utils/generalHelper/extendedError"; +import { Constant } from "types/general/constant"; + +const fetch = axios.create({ + baseURL: apiConfig.basePath, + maxRate: 5, + timeout: 10000, +}); + +fetch.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response) { + const payload = JSON.stringify( + new ExtendedError( + error.message, + error.response.data.code, + error.response.data.category, + error.response.data.reason, + ), + ); + return Promise.reject(payload); + } else if (error.request) { + const payload = JSON.stringify( + new ExtendedError( + Constant.apiNoResponse, + 500, + "noResponse", + error.message, + ), + ); + return Promise.reject(payload); + } else { + const payload = JSON.stringify( + new ExtendedError( + Constant.apiBadRequest, + 400, + "badRequest", + error.message, + ), + ); + return Promise.reject(payload); + } + }, +); + +export default fetch;