mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 20:01:36 +00:00
Write e2e test for API to make sure it's working as intended.
This commit is contained in:
+4
-1
@@ -37,4 +37,7 @@ next-env.d.ts
|
||||
!/src/pages/api/test.ts
|
||||
|
||||
# personal docs
|
||||
/docs
|
||||
/docs
|
||||
/.next
|
||||
/.vscode
|
||||
/.idea
|
||||
+1
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: "http://localhost:5000",
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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
@@ -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 |
@@ -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);
|
||||
|
||||
@@ -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*/}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user