Update protected query for folder and files.

This commit is contained in:
mbaharip
2023-04-24 14:34:06 +07:00
parent b3efeda91c
commit c8414e19a0
25 changed files with 582 additions and 360 deletions
+4
View File
@@ -34,3 +34,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
!/src/pages/api/test.ts
# personal docs
/docs
+4 -4
View File
@@ -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",
+3 -1
View File
@@ -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 (
<div className='flex flex-col gap-0'>
<div className='my-4 flex flex-col gap-0'>
<img
src={src}
alt={alt}
+4 -1
View File
@@ -1,5 +1,8 @@
module.exports = {
client_id:
"740683322791-5glqp7unsscric9g51ccdsctu5nenkgd.apps.googleusercontent.com",
redirect_uri: "http://localhost:5000/auth/google/callback",
client_secret:
"1b9a24512e48c83cf7e520a7b9442298:6b8c0dc1e9cb0c28c673ebb222f9f03f425f8566b0d9ef4a7265582a99e57499b6b215ff7d038a477c2797195a5ccf65",
refresh_token:
"fd0a8add6d49a1cbba6211eaa1e64e4e:e0a974bf57af247cf147237dea17c38e78fc6aa88e244f57ca3e373974e95dd6120949b9d320bb6c0dfaad026a2fdaac633a5c515683d1cd2eb468e6e97ac0ddf69068a5aa611fc264b9a998e8e85d20ef13a3c9ecfb527df34455f69fa5458e00e598e3441e20f8a5958c27da911a9a",
};
+3 -2
View File
@@ -46,11 +46,12 @@ const config = {
// Starting point of the drive
// Use 'root' to use My Drive as starting point
// Or use folder id to use a specific folder as starting point
rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j",
// rootFolder: "1KgPV6QB1GYT8fmn2uTfbtr9rDXqcRR0j",
rootFolder: "1p6znx1BKPsqFnyOPw49uhoc8FNglfYnD",
// If this set to true, any user can download or view protected files.
// If this set to false, only authorized users can download or view protected files.
// The authorized users URL will have a token in it that valid for 1 hour.
allowDownloadProtectedFiles: true, // If this set to true, any user can download protected files, but can't see the details of the file or folder.
allowDownloadProtectedFiles: false, // If this set to true, any user can download protected files, but can't see the details of the file or folder.
},
/* Config for readme file render */
readme: {
+71 -30
View File
@@ -1,33 +1,82 @@
import { ErrorResponse } 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,
fields: "id, name, mimeType, size",
fields: "id, name, mimeType, size, exportLinks, parents",
});
if (!config.files.allowDownloadProtectedFiles) {
const parentsArray: TFileParent[] = [];
// Fetch parents
if (
fetchFileMetadata.data.mimeType === "application/vnd.google-apps.folder"
) {
parentsArray.push({
id: fetchFileMetadata.data.id as string,
name: fetchFileMetadata.data.name as string,
});
}
let parents = fetchFileMetadata.data.parents || [];
while (parents.length > 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",
},
};
+34 -66
View File
@@ -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",
},
};
+39 -85
View File
@@ -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",
},
};
+31 -7
View File
@@ -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",
},
};
+8 -15
View File
@@ -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",
},
};
+9 -20
View File
@@ -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",
},
};
+8 -11
View File
@@ -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",
},
};
+21
View File
@@ -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,
});
}
-77
View File
@@ -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<string>("");
const copyText = useCopyText();
return (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg px-4'>
<span className='font-bold'>Generate random encryption key</span>
</div>
<div className={"divider-horizontal"} />
<p className={"mx-auto max-w-screen-md px-4 py-4 text-center"}>
On this page you can generate a random encryption key.
<br />
This key will be used to encrypt your files.
<br />
You can also copy the key to your clipboard and save it somewhere
safe.
</p>
<div
className={"mx-auto flex w-full max-w-screen-md flex-col gap-2 px-4"}
>
<div className={"flex flex-col gap-2"}>
<span className='font-bold'>Encryption key</span>
<input
type={"text"}
disabled
className={"pr-4"}
value={key}
/>
</div>
<div
className={
"mx-auto mt-4 flex w-full max-w-md flex-col items-center justify-center gap-2 tablet:flex-row tablet:gap-4"
}
>
<button
className={"flex w-full items-center justify-center"}
onClick={(e) => {
e.preventDefault();
const generated = generateRandomEncryptionKey()
.then((key) => {
setKey(key);
})
.catch((err) => {
toast.error("Failed to generate encryption key");
console.error(err);
});
}}
>
Generate
</button>
<button
className={"flex w-full items-center justify-center"}
onClick={(e) => {
e.preventDefault();
copyText(key);
}}
>
Copy
</button>
</div>
</div>
</div>
</div>
);
}
+146
View File
@@ -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<string>("");
const [allowNext, setAllowNext] = useState<boolean>(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 (
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg px-4'>
<span className='font-bold'>Encryption key</span>
</div>
<div className={"divider-horizontal"} />
<div className={"banner warning"}>
<MdWarning className={"h-6 w-6 text-red-500"} />
<span>
Make sure you don&apos;t share your encryption key with anyone.
</span>
</div>
<p className={"mx-auto max-w-screen-md px-4 py-4 text-center"}>
On this page you can generate a random encryption key, or you can
define your own.
<br />
This key will be used to encrypt your files.
<br />
<br />
You can also copy the key to your clipboard and save it somewhere
safe.
</p>
<div
className={"mx-auto flex w-full max-w-screen-md flex-col gap-4 px-4"}
>
<div className={"flex flex-col gap-2"}>
<span className='font-bold'>Encryption key</span>
<input
type={"text"}
className={"pr-4"}
value={key}
onChange={(e) => setKey(e.target.value)}
placeholder={"Enter your encryption key here..."}
/>
</div>
<div className={"mx-auto grid w-full max-w-md grid-cols-2 gap-2"}>
<button
className={"primary flex w-full items-center justify-center"}
onClick={(e) => {
e.preventDefault();
const generated = generateRandomEncryptionKey()
.then((key) => {
setKey(key);
})
.catch((err) => {
toast.error("Failed to generate encryption key");
console.error(err);
});
}}
>
Generate
</button>
<button
className={"danger flex w-full items-center justify-center"}
onClick={(e) => {
e.preventDefault();
setKey("");
}}
>
Reset
</button>
<button
className={
"secondary col-span-full flex w-full items-center justify-center"
}
onClick={(e) => {
e.preventDefault();
copyText(key);
}}
>
Copy
</button>
</div>
</div>
<div className={"divider-horizontal"} />
<div
className={
"mx-auto flex w-full items-center justify-end gap-2 tablet:gap-4"
}
>
<Link href={"/setup"}>
<button className={"secondary"}>Previous Page</button>
</Link>
<Link
href={allowNext ? "/setup/google-cloud" : ""}
onClick={async (e) => {
if (!allowNext) return;
if (!key) return;
createEncryptionKey(key).then((encryptionKey) => {
setSettingJson(encryptionKey);
});
}}
>
<button
className={"primary"}
disabled={!allowNext}
>
Next Page
</button>
</Link>
</div>
</div>
</div>
);
}
+3
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
export interface ExtendedError extends Error {
code?: number;
}
+5
View File
@@ -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: {
+16
View File
@@ -0,0 +1,16 @@
export interface JWTPayload<T> {
payload: T;
exp: number;
iat: number;
isExpired: boolean;
}
export interface ExpiredJWTPayload {
iat: number;
exp: number;
isExpired: boolean;
}
export interface ProtectionPayload {
fileId: string;
password: string;
}
+22 -13
View File
@@ -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;
}
}
+50 -2
View File
@@ -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),
};
}
+47
View File
@@ -10,3 +10,50 @@ export function generateRandomEncryptionKey(): Promise<string> {
});
});
}
export function createEncryptionKey(passphrase: string): Promise<string> {
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();
}
+9
View File
@@ -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;
}
+30
View File
@@ -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<T = any>(
token: string,
): JWTPayload<T> | ExpiredJWTPayload {
try {
return {
...(JWT.verify(
decrypt(token, process.env.ENCRYPTION_KEY as string),
process.env.JWT_KEY as string,
) as JWTPayload<T>),
isExpired: false,
};
} catch (error) {
return {
iat: 0,
exp: 0,
isExpired: true,
};
}
}
+12 -26
View File
@@ -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"