diff --git a/.gitignore b/.gitignore index eb37297..d5dd347 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +!/src/pages/api/test.ts + +# personal docs +/docs \ No newline at end of file diff --git a/package.json b/package.json index 570a303..744330e 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,8 @@ "lint": "next lint" }, "dependencies": { - "@tailwindcss/typography": "^0.5.9", - "@types/mime-types": "^2.1.1", - "@types/node": "18.15.11", "@types/react": "18.0.35", "@types/react-dom": "18.0.11", - "@types/react-pdf": "^6.2.0", "autoprefixer": "10.4.14", "axios": "^1.3.5", "googleapis": "^118.0.0", @@ -42,6 +38,10 @@ "tailwindcss": "3.3.1" }, "devDependencies": { + "@tailwindcss/typography": "^0.5.9", + "@types/jsonwebtoken": "^9.0.1", + "@types/mime-types": "^2.1.1", + "@types/node": "18.15.11", "eslint": "8.38.0", "eslint-config-next": "13.3.0", "prettier": "^2.8.7", diff --git a/src/components/utility/MarkdownRender.tsx b/src/components/utility/MarkdownRender.tsx index 27768d7..8dff60a 100644 --- a/src/components/utility/MarkdownRender.tsx +++ b/src/components/utility/MarkdownRender.tsx @@ -11,6 +11,8 @@ import rehypePrism from "rehype-prism-plus"; import { SpecialComponents } from "react-markdown/lib/ast-to-react"; import { NormalComponents } from "react-markdown/lib/complex-types"; import config from "@/config/site.config"; +import useCopyText from "@hooks/useCopyText"; +import { MdContentCopy } from "react-icons/md"; type Props = { content: string; @@ -21,7 +23,7 @@ const customComponents: Partial< > = { img({ alt, src, ...props }: any) { return ( -
+
{alt} 0) { + const fetchParents = await drive.files.get({ + fileId: parents[0], + fields: "id, name, parents", + }); + if (fetchParents.data.id === config.files.rootFolder) { + parentsArray.push({ + id: fetchParents.data.id as string, + name: fetchParents.data.name as string, + }); + break; + } + parents = fetchParents.data.parents || []; + if (!parents.length) break; + + parentsArray.push({ + id: fetchParents.data.id as string, + name: fetchParents.data.name as string, + }); + } + + // Check for password file + const validatePassword = await validateProtected( + parentsArray[0].id, + (headerHash as string) || (hash as string), + ); + if (validatePassword.isProtected && !validatePassword.valid) { + return response.status(200).json({ + success: true, + timestamp: new Date().toISOString(), + passwordRequired: true, + passwordValidated: false, + parents: [], + file: {}, + } as FileResponse); + } + } + const { name, mimeType, size } = fetchFileMetadata.data; if (mimeType === "application/vnd.google-apps.folder") { - const payload: ErrorResponse = { - success: false, - timestamp: new Date().toISOString(), - code: 400, - errors: { - message: "Cannot download folder", - reason: "badRequest", - }, - }; - - return response.status(400).json(payload); + const error = new Error("Folder cannot be downloaded") as ExtendedError; + error.cause = "badRequest"; + error.code = 400; + throw error; } response.setHeader( @@ -47,25 +96,17 @@ export default async function handler( }, ); - streamFile.data.on("error", (error: any) => { - throw error; - }); - streamFile.data.on("data", (chunk: Buffer) => { - response.write(chunk); - }); - streamFile.data.on("end", () => { - response.end(); - }); - return response.status(200); + return response.send(streamFile.data); } catch (error: any) { if (error satisfies ErrorResponse) { const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message, - reason: error.errors?.[0].reason || "internalError", + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -75,10 +116,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/files/[id]/index.ts b/src/pages/api/files/[id]/index.ts index b07830d..e513c4a 100644 --- a/src/pages/api/files/[id]/index.ts +++ b/src/pages/api/files/[id]/index.ts @@ -5,7 +5,7 @@ import { TFileParent, } from "@/types/googleapis"; import drive from "@/utils/driveClient"; -import { buildQuery } from "@/utils/driveHelper"; +import { buildQuery, validateProtected } from "@/utils/driveHelper"; import { NextApiRequest, NextApiResponse } from "next"; import config from "@config/site.config"; @@ -15,6 +15,8 @@ export default async function handler( ) { try { const { id } = request.query; + const { authorization } = request.headers; + const hash = authorization?.split(" ")[1] || null; const parentsArray: TFileParent[] = []; @@ -22,7 +24,6 @@ export default async function handler( fileId: id as string, fields: "id, name, mimeType, parents, thumbnailLink, fileExtension, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, exportLinks", - // "*", }); // Fetch parents @@ -38,7 +39,13 @@ export default async function handler( fileId: parents[0], fields: "id, name, parents", }); - if (fetchParents.data.id === config.files.rootFolder) break; + if (fetchParents.data.id === config.files.rootFolder) { + parentsArray.push({ + id: fetchParents.data.id as string, + name: fetchParents.data.name as string, + }); + break; + } parents = fetchParents.data.parents || []; if (!parents.length) break; @@ -49,59 +56,19 @@ export default async function handler( } // Check for password file - // Password checking takes too long. - // Try to find a better way or just keep this as it is. - const passwordQuery = [ - "name = '.password'", - "'me' in owners", - "trashed = false", - ]; - const fetchPassword = await drive.files.list({ - q: passwordQuery.join(" and "), - fields: "files(id, name, parents)", - pageSize: 1000, - }); - const passwordFile = fetchPassword.data.files?.find((file) => { - if (file.parents?.[0] === id) return true; - return parentsArray.some((parent) => parent.id === file.parents?.[0]); - }); - - if (passwordFile) { - // const {authorization} = request.headers; - // const userPassword = authorization?.split(" ")[1]; - // For dev purpose, get password from query - const userPassword = request.query.password as string; - if (!userPassword) - return response.status(401).json({ - success: false, - timestamp: new Date().toISOString(), - passwordRequired: true, - code: 401, - errors: { - message: "Unauthorized", - reason: "passwordRequired", - }, - }); - - const getPassword = await drive.files.get( - { - fileId: passwordFile.id as string, - alt: "media", - }, - { responseType: "text" }, - ); - - if (getPassword.data !== userPassword) - return response.status(401).json({ - success: false, - timestamp: new Date().toISOString(), - passwordRequired: true, - code: 401, - errors: { - message: "Unauthorized", - reason: "passwordWrong", - }, - }); + const validatePassword = await validateProtected( + parentsArray || (id as string), + hash as string, + ); + if (validatePassword.isProtected && !validatePassword.valid) { + return response.status(200).json({ + success: true, + timestamp: new Date().toISOString(), + passwordRequired: true, + passwordValidated: false, + parents: [], + file: {}, + } as FileResponse); } // Check if file is folder @@ -148,8 +115,8 @@ export default async function handler( success: true, timestamp: new Date().toISOString(), parents: parentsArray, - passwordRequired: !!passwordFile, - passwordValidated: true, + passwordRequired: validatePassword.isProtected, + passwordValidated: validatePassword.valid, folders, files, nextPageToken: fetchFiles.data.nextPageToken || undefined, @@ -163,8 +130,8 @@ export default async function handler( success: true, timestamp: new Date().toISOString(), parents: parentsArray, - passwordRequired: !!passwordFile, - passwordValidated: true, + passwordRequired: validatePassword.isProtected, + passwordValidated: validatePassword.valid, file: fetchFile.data, }; @@ -174,10 +141,11 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors[0].message, - reason: error.errors[0].reason, + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -187,10 +155,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/files/[id]/view.ts b/src/pages/api/files/[id]/view.ts index 0373066..84d5079 100644 --- a/src/pages/api/files/[id]/view.ts +++ b/src/pages/api/files/[id]/view.ts @@ -1,14 +1,18 @@ -import { ErrorResponse, TFileParent } from "@/types/googleapis"; +import { ErrorResponse, FileResponse, TFileParent } from "@/types/googleapis"; import drive from "@utils/driveClient"; import { NextApiRequest, NextApiResponse } from "next"; import config from "@config/site.config"; +import { validateProtected } from "@utils/driveHelper"; +import { ExtendedError } from "@/types/default"; export default async function handler( request: NextApiRequest, response: NextApiResponse, ) { try { - const { id } = request.query; + const { id, hash } = request.query; + const { authorization } = request.headers; + const headerHash = authorization?.split(" ")[1] || null; const fetchFileMetadata = await drive.files.get({ fileId: id as string, @@ -33,7 +37,13 @@ export default async function handler( fileId: parents[0], fields: "id, name, parents", }); - if (fetchParents.data.id === config.files.rootFolder) break; + if (fetchParents.data.id === config.files.rootFolder) { + parentsArray.push({ + id: fetchParents.data.id as string, + name: fetchParents.data.name as string, + }); + break; + } parents = fetchParents.data.parents || []; if (!parents.length) break; @@ -44,77 +54,29 @@ export default async function handler( } // Check for password file - // Password checking takes too long. - // Try to find a better way or just keep this as it is. - const passwordQuery = [ - "name = '.password'", - "'me' in owners", - "trashed = false", - ]; - const fetchPassword = await drive.files.list({ - q: passwordQuery.join(" and "), - fields: "files(id, name, parents)", - pageSize: 1000, - }); - const passwordFile = fetchPassword.data.files?.find((file) => { - if (file.parents?.[0] === id) return true; - return parentsArray.some((parent) => parent.id === file.parents?.[0]); - }); - - if (passwordFile) { - // const {authorization} = request.headers; - // const userPassword = authorization?.split(" ")[1]; - // For dev purpose, get password from query - const userPassword = request.query.password as string; - if (!userPassword) - return response.status(401).json({ - success: false, - timestamp: new Date().toISOString(), - passwordRequired: true, - code: 401, - errors: { - message: "Unauthorized", - reason: "passwordRequired", - }, - }); - - const getPassword = await drive.files.get( - { - fileId: passwordFile.id as string, - alt: "media", - }, - { responseType: "text" }, - ); - - if (getPassword.data !== userPassword) - return response.status(401).json({ - success: false, - timestamp: new Date().toISOString(), - passwordRequired: true, - code: 401, - errors: { - message: "Unauthorized", - reason: "passwordWrong", - }, - }); + const validatePassword = await validateProtected( + parentsArray[0].id, + (headerHash as string) || (hash as string), + ); + if (validatePassword.isProtected && !validatePassword.valid) { + return response.status(200).json({ + success: true, + timestamp: new Date().toISOString(), + passwordRequired: true, + passwordValidated: false, + parents: [], + file: {}, + } as FileResponse); } } - console.log("Serve file."); - const { name, mimeType, size, exportLinks } = fetchFileMetadata.data; + const { name, mimeType, size } = fetchFileMetadata.data; if (mimeType === "application/vnd.google-apps.folder") { - const payload: ErrorResponse = { - success: false, - timestamp: new Date().toISOString(), - code: 400, - errors: { - message: "Cannot view folder", - reason: "badRequest", - }, - }; - - return response.status(400).json(payload); + const error = new Error("Folder cannot be downloaded") as ExtendedError; + error.cause = "badRequest"; + error.code = 400; + throw error; } response.setHeader( @@ -134,25 +96,17 @@ export default async function handler( }, ); - streamFile.data.on("error", (error: any) => { - throw error; - }); - streamFile.data.on("data", (chunk: Buffer) => { - response.write(chunk); - }); - streamFile.data.on("end", () => { - response.end(); - }); - return response.status(200); + return response.send(streamFile.data); } catch (error: any) { if (error satisfies ErrorResponse) { const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors?.[0].message || error.message, - reason: error.errors?.[0].reason || "internalError", + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -162,10 +116,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/files/index.ts b/src/pages/api/files/index.ts index f4d7d1f..06298b9 100644 --- a/src/pages/api/files/index.ts +++ b/src/pages/api/files/index.ts @@ -1,6 +1,6 @@ import { ErrorResponse, FilesResponse } from "@/types/googleapis"; import drive from "@/utils/driveClient"; -import { buildQuery } from "@/utils/driveHelper"; +import { buildQuery, validateProtected } from "@/utils/driveHelper"; import { NextApiRequest, NextApiResponse } from "next"; import config from "@config/site.config"; @@ -10,6 +10,27 @@ export default async function handler( ) { try { const { pageToken } = request.query; + const { authorization } = request.headers; + const hash = authorization?.split(" ")[1] || null; + + // Check for password file + const validatePassword = await validateProtected( + config.files.rootFolder, + hash as string, + ); + if (validatePassword.isProtected && !validatePassword.valid) { + return response.status(200).json({ + success: true, + timestamp: new Date().toISOString(), + passwordRequired: true, + passwordValidated: false, + parents: [], + files: [], + folders: [], + nextPageToken: undefined, + readmeExists: false, + }); + } const fetchFiles = await drive.files.list({ q: buildQuery({ @@ -47,6 +68,8 @@ export default async function handler( const payload: FilesResponse = { success: true, timestamp: new Date().toISOString(), + passwordRequired: validatePassword.isProtected, + passwordValidated: validatePassword.valid, folders, files, nextPageToken: fetchFiles.data.nextPageToken || undefined, @@ -59,10 +82,11 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors[0].message, - reason: error.errors[0].reason, + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -72,10 +96,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/readme/[id]/index.ts b/src/pages/api/readme/[id]/index.ts index c552ef7..22d83dc 100644 --- a/src/pages/api/readme/[id]/index.ts +++ b/src/pages/api/readme/[id]/index.ts @@ -47,24 +47,17 @@ export default async function handler( }, ); - streamFile.data.on("error", (error: any) => { - throw error; - }); - streamFile.data.on("data", (chunk: Buffer) => { - response.write(chunk); - }); - streamFile.data.on("end", () => { - response.end(); - }); + return response.send(streamFile.data); } catch (error: any) { if (error satisfies ErrorResponse) { const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors[0].message, - reason: error.errors[0].reason, + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -74,10 +67,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/readme/index.ts b/src/pages/api/readme/index.ts index 8c01013..73966d0 100644 --- a/src/pages/api/readme/index.ts +++ b/src/pages/api/readme/index.ts @@ -1,9 +1,7 @@ -import { ErrorResponse, ReadmeResponse } from "@/types/googleapis"; +import { ErrorResponse } from "@/types/googleapis"; import { buildQuery } from "@/utils/driveHelper"; import drive from "@/utils/driveClient"; import { NextApiRequest, NextApiResponse } from "next"; -import { drive_v3 } from "googleapis"; -import axios from "axios"; export default async function handler( request: NextApiRequest, @@ -49,26 +47,17 @@ export default async function handler( }, ); - streamFile.data.on("error", (error: any) => { - throw error; - }); - streamFile.data.on("data", (chunk: Buffer) => { - response.write(chunk); - }); - streamFile.data.on("end", () => { - response.end(); - }); - - return response.status(200); + return response.send(streamFile.data); } catch (error: any) { if (error satisfies ErrorResponse) { const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors?.[0]?.message || "", - reason: error.errors?.[0]?.reason || "", + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -78,10 +67,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/search.ts b/src/pages/api/search.ts index 2416046..b04a64c 100644 --- a/src/pages/api/search.ts +++ b/src/pages/api/search.ts @@ -1,8 +1,4 @@ -import { - FilesResponse, - ErrorResponse, - SearchResponse, -} from "@/types/googleapis"; +import { ErrorResponse, SearchResponse } from "@/types/googleapis"; import drive from "@/utils/driveClient"; import { buildQuery } from "@/utils/driveHelper"; import { NextApiRequest, NextApiResponse } from "next"; @@ -46,10 +42,11 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: error.code, + code: error.code || 500, errors: { - message: error.errors[0].message, - reason: error.errors[0].reason, + message: + error.errors?.[0].message || error.message || "Unknown error", + reason: error.errors?.[0].reason || error.cause || "internalError", }, }; @@ -59,10 +56,10 @@ export default async function handler( const payload: ErrorResponse = { success: false, timestamp: new Date().toISOString(), - code: 500, + code: error.code || 500, errors: { - message: error.message, - reason: "internalError", + message: error.message || "Unknown error", + reason: error.cause || "internalError", }, }; diff --git a/src/pages/api/test.ts b/src/pages/api/test.ts new file mode 100644 index 0000000..7dafd7d --- /dev/null +++ b/src/pages/api/test.ts @@ -0,0 +1,21 @@ +import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; +import drive from "@utils/driveClient"; +import { decrypt } from "@utils/encryptionHelper"; +import { createJWTToken, verifyJWTToken } from "@utils/jwtHelper"; +import { hashToken, verifyHash } from "@utils/hashHelper"; +import { buildQuery } from "@utils/driveHelper"; + +export default async function handler( + request: NextApiRequest, + response: NextApiResponse, +) { + const files = await drive.files.list({ + q: "'1VMU0sQOkuI06icRRJFof-6V-NLyBWlp5' in parents", + fields: "files(id, name, mimeType, size)", + }); + const { password } = request.query; + return response.status(200).json({ + hash: hashToken(password as string), + file: files.data.files, + }); +} diff --git a/src/pages/setup.tsx b/src/pages/setup.tsx deleted file mode 100644 index 122b9b5..0000000 --- a/src/pages/setup.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { MdContentCopy } from "react-icons/md"; -import { useState } from "react"; -import { generateRandomEncryptionKey } from "@utils/encryptionHelper"; -import useCopyText from "@hooks/useCopyText"; -import { toast } from "react-toastify"; - -export default function Setup() { - const [key, setKey] = useState(""); - const copyText = useCopyText(); - - return ( -
-
-
- Generate random encryption key -
- -
- -

- On this page you can generate a random encryption key. -
- This key will be used to encrypt your files. -
- You can also copy the key to your clipboard and save it somewhere - safe. -

- -
-
- Encryption key - -
-
- - -
-
-
-
- ); -} diff --git a/src/pages/setup/encryption.tsx b/src/pages/setup/encryption.tsx new file mode 100644 index 0000000..3c9a807 --- /dev/null +++ b/src/pages/setup/encryption.tsx @@ -0,0 +1,146 @@ +import { MdContentCopy, MdWarning } from "react-icons/md"; +import { useEffect, useState } from "react"; +import { + createEncryptionKey, + generateRandomEncryptionKey, +} from "@utils/encryptionHelper"; +import useCopyText from "@hooks/useCopyText"; +import { toast } from "react-toastify"; +import Link from "next/link"; +import useLocalStorage from "@hooks/useLocalStorage"; + +export default function Encryption() { + const [settingJson, setSettingJson] = useLocalStorage("tempEncryption", ""); + const [key, setKey] = useState(""); + const [allowNext, setAllowNext] = useState(false); + const copyText = useCopyText(); + + useEffect(() => { + if (settingJson) { + setKey(settingJson); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (key) { + setAllowNext(true); + } else { + setAllowNext(false); + } + }, [key]); + + return ( +
+
+
+ Encryption key +
+ +
+ +
+ + + Make sure you don't share your encryption key with anyone. + +
+ +

+ On this page you can generate a random encryption key, or you can + define your own. +
+ This key will be used to encrypt your files. +
+
+ You can also copy the key to your clipboard and save it somewhere + safe. +

+ +
+
+ Encryption key + setKey(e.target.value)} + placeholder={"Enter your encryption key here..."} + /> +
+
+ + + +
+
+ +
+ +
+ + + + { + if (!allowNext) return; + if (!key) return; + createEncryptionKey(key).then((encryptionKey) => { + setSettingJson(encryptionKey); + }); + }} + > + + +
+
+
+ ); +} diff --git a/src/styles/markdown.css b/src/styles/markdown.css index 36e60d8..b3da497 100644 --- a/src/styles/markdown.css +++ b/src/styles/markdown.css @@ -33,6 +33,9 @@ .markdown ul { @apply list-disc ml-8 leading-loose; } + .markdown ol { + @apply list-decimal leading-loose; + } .markdown table { @apply block w-full overflow-auto border-collapse mb-4 border-spacing-0; diff --git a/src/types/default.ts b/src/types/default.ts new file mode 100644 index 0000000..745b609 --- /dev/null +++ b/src/types/default.ts @@ -0,0 +1,3 @@ +export interface ExtendedError extends Error { + code?: number; +} diff --git a/src/types/googleapis.ts b/src/types/googleapis.ts index 1997b6d..8608d2b 100644 --- a/src/types/googleapis.ts +++ b/src/types/googleapis.ts @@ -96,6 +96,11 @@ export interface ReadmeResponse extends APIResponse { file: TFile | drive_v3.Schema$File; } +export interface PasswordResponse extends APIResponse { + passwordRequired: boolean; + passwordValidated: boolean; +} + export interface ErrorResponse extends APIResponse { code: number; errors: { diff --git a/src/types/jwt.ts b/src/types/jwt.ts new file mode 100644 index 0000000..02b34e3 --- /dev/null +++ b/src/types/jwt.ts @@ -0,0 +1,16 @@ +export interface JWTPayload { + payload: T; + exp: number; + iat: number; + isExpired: boolean; +} +export interface ExpiredJWTPayload { + iat: number; + exp: number; + isExpired: boolean; +} + +export interface ProtectionPayload { + fileId: string; + password: string; +} diff --git a/src/utils/driveClient.ts b/src/utils/driveClient.ts index ae32aa6..bffd287 100644 --- a/src/utils/driveClient.ts +++ b/src/utils/driveClient.ts @@ -1,29 +1,38 @@ import apiConfig from "@config/api.config"; import { google, drive_v3 } from "googleapis"; +import { decrypt } from "@utils/encryptionHelper"; class DriveClient { - private instance: drive_v3.Drive; + instance: drive_v3.Drive; + private decryptedSecret: string = decrypt( + apiConfig.client_secret, + process.env.ENCRYPTION_KEY as string, + ); + private decryptedRefreshToken: string = decrypt( + apiConfig.refresh_token, + process.env.ENCRYPTION_KEY as string, + ); constructor() { const oauth2Client = new google.auth.OAuth2( apiConfig.client_id, - process.env.CLIENT_SECRET, - apiConfig.redirect_uri, + this.decryptedSecret, ); - oauth2Client.setCredentials({ refresh_token: process.env.REFRESH_TOKEN }); + oauth2Client.setCredentials({ refresh_token: this.decryptedRefreshToken }); this.instance = google.drive({ version: "v3", auth: oauth2Client }); } getInstance() { - if (!this.instance) { - const oauth2Client = new google.auth.OAuth2( - apiConfig.client_id, - process.env.CLIENT_SECRET, - apiConfig.redirect_uri, - ); - oauth2Client.setCredentials({ refresh_token: process.env.REFRESH_TOKEN }); - this.instance = google.drive({ version: "v3", auth: oauth2Client }); - } + // if (!this.instance) { + // const oauth2Client = new google.auth.OAuth2( + // apiConfig.client_id, + // this.decryptedSecret, + // ); + // oauth2Client.setCredentials({ + // refresh_token: process.env.REFRESH_TOKEN, + // }); + // this.instance = google.drive({ version: "v3", auth: oauth2Client }); + // } return this.instance; } } diff --git a/src/utils/driveHelper.ts b/src/utils/driveHelper.ts index 209706a..585173b 100644 --- a/src/utils/driveHelper.ts +++ b/src/utils/driveHelper.ts @@ -1,5 +1,7 @@ import drive from "@utils/driveClient"; import config from "@config/site.config"; +import { verifyHash } from "@utils/hashHelper"; +import { TFileParent } from "@/types/googleapis"; export function buildQuery({ id, @@ -31,7 +33,7 @@ export function buildQuery({ return query.join(" and "); } -export async function checkProtected(id: string) { +export async function _checkProtected(id: string) { try { const files = await drive.files.list({ q: id ? buildQuery({ id }) : buildQuery({}), @@ -50,7 +52,7 @@ export async function checkProtected(id: string) { } } -export async function validateFolderPassword( +export async function _validateFolderPassword( passwordFileId: string, password: string, ) { @@ -68,3 +70,49 @@ export async function validateFolderPassword( return false; } } + +export async function validateProtected( + fileId: string | TFileParent[], + passwordHash: string, +): Promise<{ isProtected: boolean; valid?: boolean }> { + const fetchPassword = await drive.files.list({ + q: `name = '.password' and 'me' in owners and trashed = false`, + fields: "files(id, name, parents)", + pageSize: 1000, + }); + let passwordFile; + if (typeof fileId === "string") { + passwordFile = fetchPassword.data.files?.find( + (file) => file.parents?.[0] === fileId, + ); + } + if (Array.isArray(fileId)) { + const parentsIdMap = fileId.map((parent) => parent.id); + passwordFile = fetchPassword.data.files?.find((file) => + parentsIdMap.includes(file.parents?.[0] as string), + ); + } + + console.log(fetchPassword.data.files); + + if (!passwordFile) return { isProtected: false }; + + const getPassword = await drive.files.get( + { + fileId: passwordFile.id as string, + alt: "media", + }, + { responseType: "text" }, + ); + + if (!passwordHash) + return { + isProtected: true, + valid: false, + }; + + return { + isProtected: true, + valid: verifyHash(getPassword.data as string, passwordHash), + }; +} diff --git a/src/utils/encryptionHelper.ts b/src/utils/encryptionHelper.ts index 4be5c98..1e9585c 100644 --- a/src/utils/encryptionHelper.ts +++ b/src/utils/encryptionHelper.ts @@ -10,3 +10,50 @@ export function generateRandomEncryptionKey(): Promise { }); }); } + +export function createEncryptionKey(passphrase: string): Promise { + return new Promise((resolve, reject) => { + const salt = crypto.randomBytes(16); // generate a random salt + const iterations = 100000; // number of PBKDF2 iterations + const keyLength = 32; // desired key length in bytes + + crypto.pbkdf2( + passphrase, + salt, + iterations, + keyLength, + "sha256", + (err, derivedKey) => { + if (err) throw reject(err); + const encryptionKey = derivedKey.toString("hex"); + resolve(encryptionKey); + }, + ); + }); +} + +export function encrypt(data: string, encryptionKey: string) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv( + "aes-256-cbc", + Buffer.from(encryptionKey, "hex"), + iv, + ); + let encrypted = cipher.update(data); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return iv.toString("hex") + ":" + encrypted.toString("hex"); +} + +export function decrypt(encryptedData: string, encryptionKey: string) { + const [ivString, encryptedString] = encryptedData.split(":"); + const iv = Buffer.from(ivString, "hex"); + const encrypted = Buffer.from(encryptedString, "hex"); + const decipher = crypto.createDecipheriv( + "aes-256-cbc", + Buffer.from(encryptionKey, "hex"), + iv, + ); + let decrypted = decipher.update(encrypted); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); +} diff --git a/src/utils/hashHelper.ts b/src/utils/hashHelper.ts new file mode 100644 index 0000000..0e804a9 --- /dev/null +++ b/src/utils/hashHelper.ts @@ -0,0 +1,9 @@ +import { createHash } from "crypto"; + +export function hashToken(text: string): string { + return createHash("sha256").update(text).digest("hex"); +} + +export function verifyHash(text: string, hash: string): boolean { + return hashToken(text) === hash; +} diff --git a/src/utils/jwtHelper.ts b/src/utils/jwtHelper.ts new file mode 100644 index 0000000..9f092bb --- /dev/null +++ b/src/utils/jwtHelper.ts @@ -0,0 +1,30 @@ +import JWT from "jsonwebtoken"; +import { decrypt, encrypt } from "@utils/encryptionHelper"; +import { ExpiredJWTPayload, JWTPayload } from "@/types/jwt"; + +export function createJWTToken(payload: any, expiresIn: string = "3h") { + return encrypt( + JWT.sign({ payload }, process.env.JWT_KEY as string, { expiresIn }), + process.env.ENCRYPTION_KEY as string, + ); +} + +export function verifyJWTToken( + token: string, +): JWTPayload | ExpiredJWTPayload { + try { + return { + ...(JWT.verify( + decrypt(token, process.env.ENCRYPTION_KEY as string), + process.env.JWT_KEY as string, + ) as JWTPayload), + isExpired: false, + }; + } catch (error) { + return { + iat: 0, + exp: 0, + isExpired: true, + }; + } +} diff --git a/yarn.lock b/yarn.lock index 90c24f0..2c4d0ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -233,6 +233,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== + dependencies: + "@types/node" "*" + "@types/katex@^0.11.0": version "0.11.1" resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.11.1.tgz#34de04477dcf79e2ef6c8d23b41a3d81f9ebeaf5" @@ -260,6 +267,11 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node@*": + version "18.16.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.0.tgz#4668bc392bb6938637b47e98b1f2ed5426f33316" + integrity sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ== + "@types/node@18.15.11": version "18.15.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" @@ -292,14 +304,6 @@ dependencies: "@types/react" "*" -"@types/react-pdf@^6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@types/react-pdf/-/react-pdf-6.2.0.tgz#e99d498f8f76704d15e4553197c89e254cc17278" - integrity sha512-OSCYmrfaJvpXkM5V4seUMAhUDOAOqbGQf9kwv14INyTf7AjDs2ukfkkQrLWRQ8OjWrDklbXYWh5l7pT7l0N76g== - dependencies: - "@types/react" "*" - pdfjs-dist "^2.16.105" - "@types/react@*", "@types/react@18.0.35": version "18.0.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.35.tgz#192061cb1044fe01f2d3a94272cd35dd50502741" @@ -849,11 +853,6 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dommatrix@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525" - integrity sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww== - ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -2898,14 +2897,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pdfjs-dist@^2.16.105: - version "2.16.105" - resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz#937b9c4a918f03f3979c88209d84c1ce90122c2a" - integrity sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A== - dependencies: - dommatrix "^1.0.3" - web-streams-polyfill "^3.2.1" - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -3839,11 +3830,6 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== -web-streams-polyfill@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"