Write e2e test for API to make sure it's working as intended.

This commit is contained in:
mbaharip
2023-04-25 18:30:10 +07:00
parent 333001a63e
commit ec5f4fbf5f
28 changed files with 1476 additions and 189 deletions
+4 -1
View File
@@ -37,4 +37,7 @@ next-env.d.ts
!/src/pages/api/test.ts
# personal docs
/docs
/docs
/.next
/.vscode
/.idea
+1
View File
@@ -7,5 +7,6 @@
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExceptionCaughtLocallyJS" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:5000",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});
+120
View File
@@ -0,0 +1,120 @@
const hash = require("../../src/utils/hashHelper");
const ids: { [key: string]: string } = {
file: "1K_KyC8B54En7UaD3_o7zubxwG4jtXTVK",
folder: "12fKI0g0Uvkubk1sSHCd_SDErLyc9TlWe",
protectedFolder: "1p6znx1BKPsqFnyOPw49uhoc8FNglfYnD",
protectedFile: "1NK-J1QIf0vuGnDIEHXBG2-9jXP6sKCuN",
protectedSubFolder: "1_N87OSlhPYfC3L1fGfE546LoJnCK8zBX",
protectedSubFile: "1CSKTtgXunrSHYRRp8FyMXYpNP6xK1iju",
protectedInsideProtected: "1VMU0sQOkuI06icRRJFof-6V-NLyBWlp5",
protectedFileInsideProtected: "1wXsNhp8JxOc8iBRyb98PkR1G_MijE2e0",
};
describe("Files API", () => {
it("Get root files", () => {
cy.request({
method: "GET",
url: "/api/files",
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(false);
expect(response.body.passwordValidated).to.satisfy(
(key: any) => key === true || key === undefined,
);
expect(response.body.protectedId).to.eq(undefined);
expect(response.body.folders).to.have.length.at.least(0);
expect(response.body.files).to.have.length.at.least(0);
expect(response.body.readmeExists).to.be.a("boolean");
expect(response.body.nextPageToken).to.satisfy(
(key: any) => typeof key === "string" || key === undefined,
);
});
});
it("Get file", () => {
cy.request({
method: "GET",
url: `/api/files/${ids.file}`,
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(false);
expect(response.body.passwordValidated).to.satisfy(
(key: any) => key === true || key === undefined,
);
expect(response.body.protectedId).to.eq(undefined);
expect(response.body.parents).to.have.length.at.least(0);
expect(response.body.file).to.have.property("id");
});
});
it("Get folder", () => {
cy.request({
method: "GET",
url: `/api/files/${ids.folder}`,
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(false);
expect(response.body.passwordValidated).to.satisfy(
(key: any) => key === true || key === undefined,
);
expect(response.body.protectedId).to.eq(undefined);
expect(response.body.parents).to.have.length.at.least(0);
expect(response.body.folders).to.have.length.at.least(0);
expect(response.body.files).to.have.length.at.least(0);
expect(response.body.readmeExists).to.be.a("boolean");
expect(response.body.nextPageToken).to.satisfy(
(key: any) => typeof key === "string" || key === undefined,
);
});
});
it("Get protected folder - without password", () => {
cy.request({
method: "GET",
url: `/api/files/${ids.protectedFolder}`,
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(true);
expect(response.body.passwordValidated).to.eq(false);
expect(response.body.protectedId).to.be.a("string");
});
});
it("Get protected folder - wrong password", () => {
const hashPassword = hash.hashToken("wrong password");
cy.request({
method: "GET",
url: `/api/files/${ids.protectedFolder}`,
headers: {
Authorization: `Bearer ${hashPassword}`,
},
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(true);
expect(response.body.passwordValidated).to.eq(false);
expect(response.body.protectedId).to.be.a("string");
});
});
it("Get protected folder - with password", () => {
const hashPassword = hash.hashToken("loremipsum");
cy.request({
method: "GET",
url: `/api/files/${ids.protectedFolder}`,
headers: {
Authorization: `Bearer ${hashPassword}`,
},
}).then((response) => {
expect(response.status).to.eq(200);
expect(response.body.passwordRequired).to.eq(true);
expect(response.body.passwordValidated).to.eq(true);
expect(response.body.protectedId).to.be.a("string");
expect(response.body.parents).to.have.length.at.least(0);
expect(response.body.folders).to.have.length.at.least(0);
expect(response.body.files).to.have.length.at.least(0);
expect(response.body.readmeExists).to.be.a("boolean");
expect(response.body.nextPageToken).to.satisfy(
(key: any) => typeof key === "string" || key === undefined,
);
});
});
});
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
+37
View File
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
+20
View File
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
+3 -1
View File
@@ -6,7 +6,8 @@
"dev": "next dev -p 5000",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"cypress": "cypress open"
},
"dependencies": {
"@types/react": "18.0.35",
@@ -42,6 +43,7 @@
"@types/jsonwebtoken": "^9.0.1",
"@types/mime-types": "^2.1.1",
"@types/node": "18.15.11",
"cypress": "^12.10.0",
"eslint": "8.38.0",
"eslint-config-next": "13.3.0",
"prettier": "^2.8.7",
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

+2 -7
View File
@@ -4,6 +4,7 @@ import { Fragment, useEffect, useState } from "react";
import { MdHome } from "react-icons/md";
import useLocalStorage from "@hooks/useLocalStorage";
import ReactLoading from "react-loading";
import config from "@config/site.config";
type Props = {
data: TFileParent[];
@@ -47,13 +48,7 @@ export default function Breadcrumb({ data, isLoading }: Props) {
{isLimited && !isLoading && (
<Fragment>
<span>/</span>
<Link
href={`/folder/${slicedPath?.id}`}
title={slicedPath?.name}
className='flex items-center gap-2'
>
...
</Link>
<span>...</span>
</Fragment>
)}
{!isLoading && (
@@ -7,9 +7,10 @@ import config from "@config/site.config";
type Props = {
data: TFile | drive_v3.Schema$File;
hash?: string;
};
export default function ImagePreview({ data }: Props) {
export default function ImagePreview({ data, hash }: Props) {
const [image, setImage] = useState<string>("");
const [isImageLoaded, setIsImageLoaded] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
@@ -21,7 +22,7 @@ export default function ImagePreview({ data }: Props) {
try {
let loaded = false;
const _image = new Image();
_image.src = `/api/files/${data.id}/view`;
_image.src = `/api/files/${data.id}/view${hash ? `?hash=${hash}` : ""}`;
_image.onload = () => {
setImage(_image.src);
setIsImageLoaded(true);
+8 -2
View File
@@ -11,9 +11,10 @@ import useSWR from "swr";
type Props = {
data: TFile | drive_v3.Schema$File;
hash?: string;
};
export default function FileDetails({ data }: Props) {
export default function FileDetails({ data, hash }: Props) {
const [metadata, setMetadata] = useState<{ label: string; value: string }[]>(
[],
);
@@ -72,7 +73,12 @@ export default function FileDetails({ data }: Props) {
<div className={"divider-horizontal"} />
{data.mimeType?.startsWith("image") && <ImagePreview data={data} />}
{data.mimeType?.startsWith("image") && (
<ImagePreview
data={data}
hash={hash || ""}
/>
)}
{/*{data.mimeType?.startsWith("audio") && (*/}
<div className='flex w-full items-center justify-center'>
{/*<video*/}
+8 -2
View File
@@ -27,7 +27,10 @@ export default function GridLayout({ data, pagination }: Props) {
<div className='flex w-full items-center justify-between rounded-lg px-4'>
<span className='font-bold'>Folders</span>
</div>
<div className='my-4 grid grid-cols-2 gap-4 tablet:grid-cols-5 desktop:grid-cols-7'>
<div
id={"grid-folders"}
className='my-4 grid grid-cols-2 gap-4 tablet:grid-cols-5 desktop:grid-cols-7'
>
{data?.folders.length === 0 ? (
<div className='col-span-full mb-4 flex w-full items-center justify-center'>
<span className='text-gray-500'>No folders</span>
@@ -55,7 +58,10 @@ export default function GridLayout({ data, pagination }: Props) {
<div className='flex w-full items-center justify-between rounded-lg px-4'>
<span className='font-bold'>Files</span>
</div>
<div className='my-4 grid grid-cols-2 gap-4 tablet:grid-cols-5 desktop:grid-cols-7'>
<div
id={"grid-files"}
className='my-4 grid grid-cols-2 gap-4 tablet:grid-cols-5 desktop:grid-cols-7'
>
{data?.files.length === 0 ? (
<div className='col-span-full mb-4 flex w-full items-center justify-center'>
<span className='text-gray-500'>No files</span>
+98
View File
@@ -0,0 +1,98 @@
import { useState } from "react";
import useLocalStorage from "@hooks/useLocalStorage";
import { IoMdEye, IoMdEyeOff } from "react-icons/io";
import { MdLock } from "react-icons/md";
import { useRouter } from "next/router";
import { hashToken } from "@utils/hashHelper";
type Props = {
folderId: string;
};
export default function Password({ folderId }: Props) {
const router = useRouter();
const [password, setPassword] = useState<string>("");
const [showPassword, setShowPassword] = useState<boolean>(false);
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const handleSubmit = () => {
setPasswordStorage({
...passwordStorage,
[folderId]: hashToken(password),
});
router.reload();
};
return (
<div className={"card"}>
<div className='flex w-full items-center justify-between rounded-lg px-4'>
<span className='font-bold'>Protected with password</span>
</div>
<div className={"divider-horizontal"} />
<MdLock className={"mx-auto my-4 h-24 w-24 tablet:h-32 tablet:w-32"} />
<p className={"mx-auto max-w-screen-md px-4 text-center"}>
The folder or file you are trying to access is protected with a
password.
<br />
Please enter the password to continue.
</p>
<div
className={
"mx-auto flex w-full max-w-screen-md flex-col gap-2 px-4 py-4"
}
>
<div className={"flex items-center gap-2 max-tablet:flex-col"}>
<div className={"relative flex w-full items-center"}>
<input
type={showPassword ? "text" : "password"}
className={"w-full pr-4"}
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSubmit();
}
}}
placeholder={"Enter your password here..."}
/>
<div
className={"relative right-8 flex cursor-pointer items-center"}
onClick={() => {
setShowPassword(!showPassword);
}}
>
<IoMdEye
className={`absolute h-6 w-6 ${
showPassword
? "pointer-events-none scale-y-0 opacity-0"
: "pointer-events-auto scale-y-100 opacity-100"
} transition`}
/>
<IoMdEyeOff
className={`absolute h-6 w-6 ${
!showPassword
? "pointer-events-none scale-y-0 opacity-0"
: "pointer-events-auto scale-y-100 opacity-100"
} transition`}
/>
</div>
</div>
<button
className={"primary w-full whitespace-nowrap tablet:w-fit"}
onClick={handleSubmit}
>
Submit
</button>
</div>
</div>
</div>
);
}
+12
View File
@@ -30,6 +30,7 @@ const customComponents: Partial<
<span className='my-0.5 text-center text-sm italic tablet:text-base'>
{alt} |{" "}
<a
className={"link"}
href={src}
target='_blank'
rel='noopener noreferrer'
@@ -41,6 +42,17 @@ const customComponents: Partial<
</div>
);
},
a({ href, ...props }: any) {
return (
<a
className={"link"}
href={href}
target='_blank'
rel='noopener noreferrer'
{...props}
/>
);
},
};
export default function MarkdownRender({ content }: Props) {
+3 -3
View File
@@ -1,8 +1,8 @@
module.exports = {
client_id:
"740683322791-5glqp7unsscric9g51ccdsctu5nenkgd.apps.googleusercontent.com",
"126409166174-l2unckghm8d5m1gp3deu0uaps5son64f.apps.googleusercontent.com",
client_secret:
"1b9a24512e48c83cf7e520a7b9442298:6b8c0dc1e9cb0c28c673ebb222f9f03f425f8566b0d9ef4a7265582a99e57499b6b215ff7d038a477c2797195a5ccf65",
"54f0505d11a8fe04d22ec642cdde4728:a6f911c70b0305a51160a2133a9bd2ada3c120567857002311f17bf29cc69e07d7f1a9ca20fd119d684191aee6e3866a",
refresh_token:
"fd0a8add6d49a1cbba6211eaa1e64e4e:e0a974bf57af247cf147237dea17c38e78fc6aa88e244f57ca3e373974e95dd6120949b9d320bb6c0dfaad026a2fdaac633a5c515683d1cd2eb468e6e97ac0ddf69068a5aa611fc264b9a998e8e85d20ef13a3c9ecfb527df34455f69fa5458e00e598e3441e20f8a5958c27da911a9a",
"b6ef7852ce97441fb6a43e1701b788f1:d96b5bc1624455536b0f5934a1d39e223d11bacfb2edcbfe69be568c7230bec69d741bc4c789d5c435bf575ab70dfd9a4c350c70562b73ec4f6e4903f0aef825fdd2dd2b1b8c42cb78f0fed1d74be7785e9a48082aac76b23cb3dc419ce11e9a8c61a586423913a1b7c2d996a5306816",
};
+3 -2
View File
@@ -46,8 +46,9 @@ 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: "1p6znx1BKPsqFnyOPw49uhoc8FNglfYnD",
// rootFolder: "root",
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.
+3
View File
@@ -66,6 +66,7 @@ export default async function handler(
timestamp: new Date().toISOString(),
passwordRequired: true,
passwordValidated: false,
protectedId: validatePassword.protectedId,
parents: [],
file: {},
} as FileResponse);
@@ -117,6 +118,7 @@ export default async function handler(
parents: parentsArray,
passwordRequired: validatePassword.isProtected,
passwordValidated: validatePassword.valid,
protectedId: validatePassword.protectedId,
folders,
files,
nextPageToken: fetchFiles.data.nextPageToken || undefined,
@@ -132,6 +134,7 @@ export default async function handler(
parents: parentsArray,
passwordRequired: validatePassword.isProtected,
passwordValidated: validatePassword.valid,
protectedId: validatePassword.protectedId,
file: fetchFile.data,
};
+2
View File
@@ -24,6 +24,7 @@ export default async function handler(
timestamp: new Date().toISOString(),
passwordRequired: true,
passwordValidated: false,
protectedId: config.files.rootFolder,
parents: [],
files: [],
folders: [],
@@ -70,6 +71,7 @@ export default async function handler(
timestamp: new Date().toISOString(),
passwordRequired: validatePassword.isProtected,
passwordValidated: validatePassword.valid,
protectedId: validatePassword.protectedId,
folders,
files,
nextPageToken: fetchFiles.data.nextPageToken || undefined,
+58 -4
View File
@@ -1,25 +1,53 @@
import useSWR from "swr";
import fetcher from "@utils/swrFetch";
import { ErrorResponse, FileResponse, TFileParent } from "@/types/googleapis";
import {
ErrorResponse,
FileResponse,
FilesResponse,
TFileParent,
} from "@/types/googleapis";
import Breadcrumb from "@/components/Breadcrumb";
import { useEffect, useState } from "react";
import LoadingFeedback from "@components/APIFeedback/Loading";
import ErrorFeedback from "@components/APIFeedback/Error";
import { useRouter } from "next/router";
import FileDetails from "@components/layout/FileDetails";
import useLocalStorage from "@hooks/useLocalStorage";
import axios from "axios";
import { GetServerSidePropsContext } from "next";
export default function File() {
type Props = {
passwordParent?: string;
};
export default function File({ passwordParent }: Props) {
const router = useRouter();
const { id } = router.query;
const [data, setData] = useState<FileResponse>();
const [dataLoading, setDataLoading] = useState<boolean>(true);
const [passwordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const {
data: swrData,
error,
isLoading,
} = useSWR<FileResponse, ErrorResponse>(`/api/files/${id}`, fetcher);
} = useSWR<FileResponse, ErrorResponse>(`/api/files/${id}`, (url, headers) =>
axios
.get<FileResponse>(url, {
headers: {
Authorization: `Bearer ${
passwordStorage?.[passwordParent as string] ||
passwordStorage?.[id as string] ||
""
}`,
...headers,
},
})
.then((res) => res.data),
);
useEffect(() => {
setDataLoading(true);
@@ -51,7 +79,33 @@ export default function File() {
{isLoading && <LoadingFeedback message={"Loading file details..."} />}
{!isLoading && error && <ErrorFeedback message={error.errors?.message} />}
{!isLoading && !error && data && <FileDetails data={data.file} />}
{!isLoading && !error && data && (
<FileDetails
data={data.file}
hash={passwordStorage?.[passwordParent as string] || ""}
/>
)}
</div>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { id } = context.query;
const passwordParent = await axios.get(
`http://localhost:5000/api/files/${id}`,
);
if (passwordParent) {
return {
props: {
passwordParent: passwordParent.data.protectedId || null,
},
};
} else {
return {
props: {
passwordParent: "",
},
};
}
}
+103 -52
View File
@@ -14,8 +14,14 @@ import ListLayout from "@components/layout/Files/ListLayout";
import LoadingFeedback from "@components/APIFeedback/Loading";
import ErrorFeedback from "@components/APIFeedback/Error";
import { useRouter } from "next/router";
import axios from "axios";
import Password from "@components/layout/Password";
import { GetServerSidePropsContext } from "next";
export default function Folder() {
type Props = {
passwordParent?: string;
};
export default function Folder({ passwordParent }: Props) {
const router = useRouter();
const { id } = router.query;
@@ -25,6 +31,10 @@ export default function Folder() {
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
const [passwordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const getNextKey = buildNextKey(`/api/files/${id}`);
const {
data: swrData,
@@ -32,7 +42,20 @@ export default function Folder() {
isLoading,
size,
setSize,
} = useSWRInfinite<FilesResponse, ErrorResponse>(getNextKey, fetcher);
} = useSWRInfinite<FilesResponse, ErrorResponse>(getNextKey, (url, headers) =>
axios
.get<FilesResponse>(url, {
headers: {
Authorization: `Bearer ${
passwordStorage?.[passwordParent as string] ||
passwordStorage?.[id as string] ||
""
}`,
...headers,
},
})
.then((res) => res.data),
);
const {
data: readmeData,
error: readmeError,
@@ -44,7 +67,7 @@ export default function Folder() {
isLoadingInitialData ||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined");
const isEmpty =
swrData?.[0]?.files.length === 0 && swrData?.[0]?.folders.length === 0;
swrData?.[0]?.files?.length === 0 && swrData?.[0]?.folders?.length === 0;
const isReachingEnd =
isEmpty ||
(swrData &&
@@ -82,62 +105,90 @@ export default function Folder() {
{!isLoading && error && <ErrorFeedback message={error.errors?.message} />}
{!isLoading && !error && data && (
<>
{isReadmeExists && config.readme.position === "start" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
{data.passwordRequired && !data.passwordValidated && (
<Password folderId={id as string} />
)}
{(data.passwordValidated || !data.passwordRequired) && (
<>
{isReadmeExists && config.readme.position === "start" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
<div className={"card"}>
{layoutStyle === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
{layoutStyle === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
</div>
<div className={"card"}>
{layoutStyle === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
{layoutStyle === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
</div>
{isReadmeExists && config.readme.position === "end" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
{isReadmeExists && config.readme.position === "end" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
</>
)}
</>
)}
</div>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { id } = context.query;
const passwordParent = await axios.get(
`http://localhost:5000/api/files/${id}`,
);
if (passwordParent) {
return {
props: {
passwordParent: passwordParent.data.protectedId || null,
},
};
} else {
return {
props: {
passwordParent: "",
},
};
}
}
+83 -53
View File
@@ -13,6 +13,9 @@ import SwitchLayout from "@components/utility/SwitchLayout";
import ListLayout from "@components/layout/Files/ListLayout";
import LoadingFeedback from "@components/APIFeedback/Loading";
import ErrorFeedback from "@components/APIFeedback/Error";
import { hashToken } from "@utils/hashHelper";
import axios from "axios";
import Password from "@components/layout/Password";
export default function Home() {
const [data, setData] = useState<FilesResponse>();
@@ -21,6 +24,10 @@ export default function Home() {
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
[key: string]: string;
}>("passwordStorage", {});
const getNextKey = buildNextKey("/api/files/");
const {
data: swrData,
@@ -28,19 +35,33 @@ export default function Home() {
isLoading,
size,
setSize,
} = useSWRInfinite<FilesResponse, ErrorResponse>(getNextKey, fetcher);
mutate,
} = useSWRInfinite<FilesResponse, ErrorResponse>(getNextKey, (url, headers) =>
axios
.get<FilesResponse>(url, {
headers: {
Authorization: `Bearer ${
passwordStorage?.[config.files.rootFolder] || ""
}`,
...headers,
},
})
.then((res) => res.data),
);
const {
data: readmeData,
error: readmeError,
isLoading: readmeLoading,
} = useSWR("/api/readme/", fetcher);
} = useSWR("/api/readme/", fetcher, {
shouldRetryOnError: false,
});
const isLoadingInitialData = !swrData && !error;
const isLoadingMore =
isLoadingInitialData ||
(size > 0 && swrData && typeof swrData[size - 1] === "undefined");
const isEmpty =
swrData?.[0]?.files.length === 0 && swrData?.[0]?.folders.length === 0;
swrData?.[0]?.files?.length === 0 && swrData?.[0]?.folders?.length === 0;
const isReachingEnd =
isEmpty ||
(swrData &&
@@ -75,62 +96,71 @@ export default function Home() {
</div>
{isLoading && <LoadingFeedback message={"Loading file..."} />}
{!isLoading && error && <ErrorFeedback message={error.errors?.message} />}
{!isLoading && error && (
<ErrorFeedback message={error.errors?.message || "Unknown error"} />
)}
{!isLoading && !error && data && (
<>
{isReadmeExists && config.readme.position === "start" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
{data.passwordRequired && !data.passwordValidated && (
<Password folderId={config.files.rootFolder} />
)}
{(data.passwordValidated || !data.passwordRequired) && (
<>
{isReadmeExists && config.readme.position === "start" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
<div className={"card"}>
{layoutStyle === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
{layoutStyle === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
</div>
<div className={"card"}>
{layoutStyle === "list" && (
<ListLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
{layoutStyle === "grid" && (
<GridLayout
data={data}
pagination={{
swrData,
isLoadingMore,
isReachingEnd,
size,
setSize,
}}
/>
)}
</div>
{isReadmeExists && config.readme.position === "end" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
{isReadmeExists && config.readme.position === "end" && (
<div className='card w-full'>
{readmeLoading && (
<LoadingFeedback message={"Loading readme..."} />
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
)}
{readmeError && !readmeLoading && (
<ErrorFeedback message={readmeError.errors?.message} />
)}
{readmeData && !readmeLoading && (
<MarkdownRender content={readmeData as string} />
)}
</div>
</>
)}
</>
)}
+3 -4
View File
@@ -71,11 +71,10 @@ html, body {
}
a {
@apply hover:opacity-75 transition duration-150;
@apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 underline;
}
a:has(div.items), a:has(span) {
@apply text-zinc-900 dark:text-zinc-100 hover:text-zinc-800 dark:hover:text-zinc-200;
@apply no-underline;
}
a.link {
@apply text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 underline underline-offset-2;
}
+2
View File
@@ -74,6 +74,7 @@ export type TFile = {
export interface FilesResponse extends APIResponse {
passwordRequired: boolean;
passwordValidated?: boolean;
protectedId?: string;
parents?: TFileParent[];
folders: (TFolder | drive_v3.Schema$File)[];
files: (TFile | drive_v3.Schema$File)[];
@@ -85,6 +86,7 @@ export interface FileResponse extends APIResponse {
parents?: TFileParent[];
passwordRequired: boolean;
passwordValidated?: boolean;
protectedId?: string;
file: TFile | drive_v3.Schema$File;
}
+10 -10
View File
@@ -23,16 +23,16 @@ class DriveClient {
}
getInstance() {
// 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 });
// }
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;
}
}
+6 -3
View File
@@ -74,27 +74,28 @@ export async function _validateFolderPassword(
export async function validateProtected(
fileId: string | TFileParent[],
passwordHash: string,
): Promise<{ isProtected: boolean; valid?: boolean }> {
): Promise<{ isProtected: boolean; valid?: boolean; protectedId?: string }> {
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;
let protectedId;
if (typeof fileId === "string") {
passwordFile = fetchPassword.data.files?.find(
(file) => file.parents?.[0] === fileId,
);
protectedId = 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),
);
protectedId = passwordFile?.parents?.[0];
}
console.log(fetchPassword.data.files);
if (!passwordFile) return { isProtected: false };
const getPassword = await drive.files.get(
@@ -109,10 +110,12 @@ export async function validateProtected(
return {
isProtected: true,
valid: false,
protectedId,
};
return {
isProtected: true,
valid: verifyHash(getPassword.data as string, passwordHash),
protectedId,
};
}
+17 -17
View File
@@ -1,28 +1,28 @@
import axios from "axios";
import {FilesResponse} from "@/types/googleapis";
import { FilesResponse } from "@/types/googleapis";
const fetcher = async <T>(url: string) =>
axios.get<T>(url).then((res) => res.data);
const fetcher = async <T>(url: string, headers: Record<string, string> = {}) =>
axios.get<T>(url, { headers }).then((res) => res.data);
function getNextKey(
pageIndex: number,
previousPageData: FilesResponse,
pageIndex: number,
previousPageData: FilesResponse,
): string | null {
if (previousPageData && !previousPageData.nextPageToken) {
return null;
}
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
return `/api/files?pageToken=${pageToken}`;
if (previousPageData && !previousPageData.nextPageToken) {
return null;
}
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
return `/api/files?pageToken=${pageToken}`;
}
export function buildNextKey(apiURL:string) {
return (pageIndex: number, previousPageData: FilesResponse) => {
if (previousPageData && !previousPageData.nextPageToken) {
return null;
}
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
return `${apiURL}?pageToken=${pageToken}`;
export function buildNextKey(apiURL: string) {
return (pageIndex: number, previousPageData: FilesResponse) => {
if (previousPageData && !previousPageData.nextPageToken) {
return null;
}
const pageToken = previousPageData ? previousPageData.nextPageToken : "";
return `${apiURL}?pageToken=${pageToken}`;
};
}
export default fetcher;
+851 -26
View File
File diff suppressed because it is too large Load Diff