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
-
-
-
- {
- e.preventDefault();
-
- const generated = generateRandomEncryptionKey()
- .then((key) => {
- setKey(key);
- })
- .catch((err) => {
- toast.error("Failed to generate encryption key");
- console.error(err);
- });
- }}
- >
- Generate
-
- {
- e.preventDefault();
- copyText(key);
- }}
- >
- Copy
-
-
-
-
-
- );
-}
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..."}
+ />
+
+
+ {
+ e.preventDefault();
+
+ const generated = generateRandomEncryptionKey()
+ .then((key) => {
+ setKey(key);
+ })
+ .catch((err) => {
+ toast.error("Failed to generate encryption key");
+ console.error(err);
+ });
+ }}
+ >
+ Generate
+
+ {
+ e.preventDefault();
+
+ setKey("");
+ }}
+ >
+ Reset
+
+ {
+ e.preventDefault();
+ copyText(key);
+ }}
+ >
+ Copy
+
+
+
+
+
+
+
+
+ Previous Page
+
+ {
+ if (!allowNext) return;
+ if (!key) return;
+ createEncryptionKey(key).then((encryptionKey) => {
+ setSettingJson(encryptionKey);
+ });
+ }}
+ >
+
+ Next Page
+
+
+
+
+
+ );
+}
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"