mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Protected folder and files implemented.
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "13.3.0",
|
||||
"next-seo": "^6.0.0",
|
||||
"nextjs-progressbar": "^0.0.16",
|
||||
"postcss": "8.4.22",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TFileParent } from "@/types/googleapis";
|
||||
import Link from "next/link";
|
||||
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";
|
||||
|
||||
@@ -21,6 +20,10 @@ export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
useEffect(() => {
|
||||
// setIsLoading(true);
|
||||
if (data.length > 0) {
|
||||
const findRoot = data.find((item) => item.id === config.files.rootFolder);
|
||||
if (findRoot) {
|
||||
data = data.filter((item) => item.id !== config.files.rootFolder);
|
||||
}
|
||||
setLimitedPath(data.slice(0, limitItem).reverse());
|
||||
setSlicedPath(data.slice(limitItem)[0]);
|
||||
setIsLimited(data.length > limitItem);
|
||||
@@ -30,51 +33,52 @@ export default function Breadcrumb({ data, isLoading }: Props) {
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Link
|
||||
href='/'
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<MdHome />
|
||||
<span>Root</span>
|
||||
</Link>
|
||||
{isLoading && (
|
||||
{isLoading ? (
|
||||
<ReactLoading
|
||||
type='spin'
|
||||
width={20}
|
||||
height={20}
|
||||
className={"loading"}
|
||||
/>
|
||||
)}
|
||||
{isLimited && !isLoading && (
|
||||
<Fragment>
|
||||
<span>/</span>
|
||||
<span>...</span>
|
||||
</Fragment>
|
||||
)}
|
||||
{!isLoading && (
|
||||
) : (
|
||||
<>
|
||||
{limitedPath.map((parent, idx) => (
|
||||
<Fragment key={parent.id}>
|
||||
<span>/</span>
|
||||
|
||||
{idx === limitedPath.length - 1 ? (
|
||||
<span className='flex cursor-default items-center gap-2 font-bold'>
|
||||
{parent.name}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/folder/${parent.id}`}
|
||||
className={`flex items-center gap-2 ${
|
||||
idx === limitedPath.length - 1
|
||||
? "cursor-default font-bold"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{parent.name}
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href='/'
|
||||
className='flex items-center gap-2'
|
||||
>
|
||||
<MdHome />
|
||||
<span>Root</span>
|
||||
</Link>
|
||||
{isLimited && (
|
||||
<Fragment>
|
||||
<span className={"cursor-default"}>/</span>
|
||||
<span className={"cursor-default"}>...</span>
|
||||
</Fragment>
|
||||
))}
|
||||
)}
|
||||
<>
|
||||
{limitedPath.map((parent, idx) => (
|
||||
<Fragment key={parent.id}>
|
||||
<span className={"cursor-default"}>/</span>
|
||||
|
||||
{idx === limitedPath.length - 1 ? (
|
||||
<span className='flex cursor-default cursor-default items-center gap-2 font-bold'>
|
||||
{parent.name}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/folder/${parent.id}`}
|
||||
className={`flex items-center gap-2 ${
|
||||
idx === limitedPath.length - 1
|
||||
? "cursor-default font-bold"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{parent.name}
|
||||
</Link>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import LoadingFeedback from "@components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "@components/APIFeedback/Error";
|
||||
import config from "@config/site.config";
|
||||
import { reverseString } from "@utils/hashHelper";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
@@ -22,7 +23,9 @@ export default function ImagePreview({ data, hash }: Props) {
|
||||
try {
|
||||
let loaded = false;
|
||||
const _image = new Image();
|
||||
_image.src = `/api/files/${data.id}/view${hash ? `?hash=${hash}` : ""}`;
|
||||
_image.src = `/api/files/${data.id}/view${
|
||||
hash ? `?hash=${reverseString(hash)}` : ""
|
||||
}`;
|
||||
_image.onload = () => {
|
||||
setImage(_image.src);
|
||||
setIsImageLoaded(true);
|
||||
|
||||
@@ -3,43 +3,55 @@ import { drive_v3 } from "googleapis";
|
||||
import { MdCopyAll, MdDownload, MdOpenInBrowser } from "react-icons/md";
|
||||
import Link from "next/link";
|
||||
import { toast } from "react-toastify";
|
||||
import config from "@config/site.config";
|
||||
import { reverseString } from "@utils/hashHelper";
|
||||
|
||||
type Props = {
|
||||
data: TFile | drive_v3.Schema$File;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
export default function DetailsButtons({ data }: Props) {
|
||||
export default function DetailsButtons({ data, hash }: Props) {
|
||||
return (
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<Link
|
||||
href={`/api/files/${data.id}/download`}
|
||||
className={
|
||||
"flex w-full items-center justify-center gap-2 rounded-lg bg-blue-500 py-4 text-center text-zinc-100 dark:bg-blue-400 tablet:py-2"
|
||||
}
|
||||
href={`/api/files/${data.id}/download${
|
||||
hash ? `?hash=${reverseString(hash)}` : ""
|
||||
}`}
|
||||
target={"_blank"}
|
||||
rel={"noopener noreferrer"}
|
||||
>
|
||||
<MdDownload />
|
||||
Download file
|
||||
<button
|
||||
className={
|
||||
"primary flex w-full items-center justify-center gap-2 py-4 tablet:py-2"
|
||||
}
|
||||
>
|
||||
<MdDownload />
|
||||
Download file
|
||||
</button>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/api/files/${data.id}/view`}
|
||||
className={
|
||||
"flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-500 py-4 text-center text-zinc-100 dark:bg-zinc-500 tablet:py-2"
|
||||
}
|
||||
href={`/api/files/${data.id}/view${
|
||||
hash ? `?hash=${reverseString(hash)}` : ""
|
||||
}`}
|
||||
target={"_blank"}
|
||||
rel={"noopener noreferrer"}
|
||||
>
|
||||
<MdOpenInBrowser />
|
||||
Open in new tab
|
||||
<button
|
||||
className={
|
||||
"secondary flex w-full items-center justify-center gap-2 py-4 tablet:py-2"
|
||||
}
|
||||
>
|
||||
<MdOpenInBrowser />
|
||||
Open in new tab
|
||||
</button>
|
||||
</Link>
|
||||
<Link
|
||||
href={``}
|
||||
className={
|
||||
"flex w-full items-center justify-center gap-2 rounded-lg bg-zinc-500 py-4 text-center text-zinc-100 dark:bg-zinc-500 tablet:py-2"
|
||||
}
|
||||
target={"_blank"}
|
||||
rel={"noopener noreferrer"}
|
||||
<button
|
||||
className={`flex w-full items-center justify-center gap-2 py-4 tablet:py-2 ${
|
||||
config.files.allowDownloadProtectedWithoutAccess
|
||||
? "secondary"
|
||||
: "danger"
|
||||
}`}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
if (!window.navigator) {
|
||||
@@ -48,14 +60,28 @@ export default function DetailsButtons({ data }: Props) {
|
||||
}
|
||||
|
||||
const host = window.location.host;
|
||||
const url = `${host}/api/files/${data.id}/view`;
|
||||
const url = `${host}/api/files/${data.id}/view${
|
||||
hash ? `?hash=${reverseString(hash)}` : ""
|
||||
}`;
|
||||
await window.navigator.clipboard.writeText(url);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<MdCopyAll />
|
||||
Copy direct link
|
||||
</Link>
|
||||
</button>
|
||||
{!config.files.allowDownloadProtectedWithoutAccess && (
|
||||
<div className={"banner warning text-sm"}>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<div className={"font-bold"}>
|
||||
Copying the direct link will also copy the access token.
|
||||
</div>
|
||||
<p className={"text-sm"}>
|
||||
Only share the direct link with people you trust.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ export default function FileDetails({ data, hash }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className={"card"}>
|
||||
<DetailsButtons data={data} />
|
||||
<DetailsButtons
|
||||
data={data}
|
||||
hash={hash || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,11 @@ export default function Navbar() {
|
||||
<div className='flex-grow'></div>
|
||||
|
||||
{/* Search */}
|
||||
<div className='interactive flex items-center gap-2'>
|
||||
<div
|
||||
id={"search-modal-toggle"}
|
||||
className='interactive flex items-center gap-2'
|
||||
role={"button"}
|
||||
>
|
||||
<div
|
||||
className='relative flex aspect-square h-6 w-6 cursor-pointer items-center justify-center text-inherit'
|
||||
onClick={() => setIsSearching(true)}
|
||||
@@ -117,8 +121,10 @@ export default function Navbar() {
|
||||
</div>
|
||||
{/* Dark mode */}
|
||||
<div
|
||||
id={"btn-theme-toggle"}
|
||||
className='interactive relative flex aspect-square h-6 w-6 cursor-pointer items-center justify-center text-inherit'
|
||||
onClick={handleDarkMode}
|
||||
role={"button"}
|
||||
>
|
||||
<MdDarkMode
|
||||
className={`absolute left-0 h-full w-full transition-all duration-150 ${
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { Dispatch, SetStateAction, useEffect, 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";
|
||||
import ReactLoading from "react-loading";
|
||||
|
||||
type Props = {
|
||||
folderId: string;
|
||||
inputCallback: (data: { [p: string]: string }) => void;
|
||||
};
|
||||
export default function Password({ folderId }: Props) {
|
||||
export default function Password({ folderId, inputCallback }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
@@ -17,12 +20,16 @@ export default function Password({ folderId }: Props) {
|
||||
}>("passwordStorage", {});
|
||||
|
||||
const handleSubmit = () => {
|
||||
setPasswordStorage({
|
||||
inputCallback({
|
||||
...passwordStorage,
|
||||
[folderId]: hashToken(password),
|
||||
});
|
||||
|
||||
router.reload();
|
||||
// setPasswordStorage({
|
||||
// ...passwordStorage,
|
||||
// [folderId]: hashToken(password),
|
||||
// });
|
||||
//
|
||||
// callback();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -89,7 +96,16 @@ export default function Password({ folderId }: Props) {
|
||||
className={"primary w-full whitespace-nowrap tablet:w-fit"}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{/*{isLoading ? (*/}
|
||||
{/* <ReactLoading*/}
|
||||
{/* type='spin'*/}
|
||||
{/* width={16}*/}
|
||||
{/* height={16}*/}
|
||||
{/* className={"loading"}*/}
|
||||
{/* />*/}
|
||||
{/*) : (*/}
|
||||
Submit
|
||||
{/*)}*/}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ const config = {
|
||||
// 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: false, // If this set to true, any user can download protected files, but can't see the details of the file or folder.
|
||||
allowDownloadProtectedWithoutAccess: 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: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NextApiRequest, NextApiResponse } from "next";
|
||||
import config from "@config/site.config";
|
||||
import { validateProtected } from "@utils/driveHelper";
|
||||
import { ExtendedError } from "@/types/default";
|
||||
import { reverseString } from "@utils/hashHelper";
|
||||
|
||||
export default async function handler(
|
||||
request: NextApiRequest,
|
||||
@@ -19,9 +20,13 @@ export default async function handler(
|
||||
fields: "id, name, mimeType, size, exportLinks, parents",
|
||||
});
|
||||
|
||||
if (!config.files.allowDownloadProtectedFiles) {
|
||||
if (!config.files.allowDownloadProtectedWithoutAccess) {
|
||||
const parentsArray: TFileParent[] = [];
|
||||
|
||||
let validHash = headerHash as string;
|
||||
if (hash) {
|
||||
validHash = reverseString(hash as string);
|
||||
}
|
||||
// Fetch parents
|
||||
if (
|
||||
fetchFileMetadata.data.mimeType === "application/vnd.google-apps.folder"
|
||||
@@ -54,10 +59,7 @@ export default async function handler(
|
||||
}
|
||||
|
||||
// Check for password file
|
||||
const validatePassword = await validateProtected(
|
||||
parentsArray,
|
||||
(headerHash as string) || (hash as string),
|
||||
);
|
||||
const validatePassword = await validateProtected(parentsArray, validHash);
|
||||
if (validatePassword.isProtected && !validatePassword.valid) {
|
||||
return response.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { NextApiRequest, NextApiResponse } from "next";
|
||||
import config from "@config/site.config";
|
||||
import { validateProtected } from "@utils/driveHelper";
|
||||
import { ExtendedError } from "@/types/default";
|
||||
import { decrypt } from "@utils/encryptionHelper";
|
||||
import { verify } from "jsonwebtoken";
|
||||
import { reverseString } from "@utils/hashHelper";
|
||||
|
||||
export default async function handler(
|
||||
request: NextApiRequest,
|
||||
@@ -11,6 +14,7 @@ export default async function handler(
|
||||
) {
|
||||
try {
|
||||
const { id, hash } = request.query;
|
||||
const { vector, data } = request.query;
|
||||
const { authorization } = request.headers;
|
||||
const headerHash = authorization?.split(" ")[1] || null;
|
||||
|
||||
@@ -19,9 +23,14 @@ export default async function handler(
|
||||
fields: "id, name, mimeType, size, exportLinks, parents",
|
||||
});
|
||||
|
||||
if (!config.files.allowDownloadProtectedFiles) {
|
||||
if (!config.files.allowDownloadProtectedWithoutAccess) {
|
||||
const parentsArray: TFileParent[] = [];
|
||||
|
||||
let validHash = headerHash as string;
|
||||
if (hash) {
|
||||
validHash = reverseString(hash as string);
|
||||
}
|
||||
|
||||
// Fetch parents
|
||||
if (
|
||||
fetchFileMetadata.data.mimeType === "application/vnd.google-apps.folder"
|
||||
@@ -54,10 +63,7 @@ export default async function handler(
|
||||
}
|
||||
|
||||
// Check for password file
|
||||
const validatePassword = await validateProtected(
|
||||
parentsArray,
|
||||
(headerHash as string) || (hash as string),
|
||||
);
|
||||
const validatePassword = await validateProtected(parentsArray, validHash);
|
||||
if (validatePassword.isProtected && !validatePassword.valid) {
|
||||
return response.status(200).json({
|
||||
success: true,
|
||||
|
||||
+96
-39
@@ -1,13 +1,7 @@
|
||||
import useSWR from "swr";
|
||||
import fetcher from "@utils/swrFetch";
|
||||
import {
|
||||
ErrorResponse,
|
||||
FileResponse,
|
||||
FilesResponse,
|
||||
TFileParent,
|
||||
} from "@/types/googleapis";
|
||||
import { ErrorResponse, FileResponse, TFileParent } from "@/types/googleapis";
|
||||
import Breadcrumb from "@/components/Breadcrumb";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import LoadingFeedback from "@components/APIFeedback/Loading";
|
||||
import ErrorFeedback from "@components/APIFeedback/Error";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -15,6 +9,7 @@ import FileDetails from "@components/layout/FileDetails";
|
||||
import useLocalStorage from "@hooks/useLocalStorage";
|
||||
import axios from "axios";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Password from "@components/layout/Password";
|
||||
|
||||
type Props = {
|
||||
passwordParent?: string;
|
||||
@@ -24,33 +19,51 @@ export default function File({ passwordParent }: Props) {
|
||||
const { id } = router.query;
|
||||
|
||||
const [data, setData] = useState<FileResponse>();
|
||||
const [dataLoading, setDataLoading] = useState<boolean>(true);
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
|
||||
|
||||
const [passwordStorage] = useLocalStorage<{
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
const [password, setPassword] = useState<{ [p: string]: string }>(
|
||||
passwordStorage,
|
||||
);
|
||||
|
||||
const {
|
||||
data: swrData,
|
||||
error,
|
||||
isLoading,
|
||||
} = 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),
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWR<FileResponse, ErrorResponse>(
|
||||
`/api/files/${id}`,
|
||||
(url, headers) =>
|
||||
axios
|
||||
.get<FileResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
password?.[passwordParent as string] ||
|
||||
password?.[id as string] ||
|
||||
passwordStorage?.[passwordParent as string] ||
|
||||
passwordStorage?.[id as string] ||
|
||||
""
|
||||
}`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDataLoading(true);
|
||||
setGlobalLoading(true);
|
||||
if (swrData) {
|
||||
const parentsArray: TFileParent[] | undefined = swrData.parents;
|
||||
parentsArray?.unshift({
|
||||
@@ -62,28 +75,67 @@ export default function File({ passwordParent }: Props) {
|
||||
...swrData,
|
||||
};
|
||||
setData(payload);
|
||||
setDataLoading(false);
|
||||
setGlobalLoading(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [swrData, error, isLoading]);
|
||||
}, [swrData, error, isLoading, isValidating, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isValidating) {
|
||||
setGlobalLoading(false);
|
||||
} else {
|
||||
setGlobalLoading(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading, isValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
|
||||
const inputPassCallback = useCallback(
|
||||
(data: { [p: string]: string }) => {
|
||||
setGlobalLoading(true);
|
||||
setPasswordStorage(data);
|
||||
setPassword(data);
|
||||
},
|
||||
[setPasswordStorage],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={dataLoading}
|
||||
/>
|
||||
</div>
|
||||
{globalLoading && <LoadingFeedback message={"Loading file details..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message} />
|
||||
)}
|
||||
{!globalLoading && !error && data && (
|
||||
<>
|
||||
{data.passwordRequired && !data.passwordValidated && (
|
||||
<Password
|
||||
folderId={(passwordParent as string) || (id as string)}
|
||||
inputCallback={inputPassCallback}
|
||||
/>
|
||||
)}
|
||||
{(data.passwordValidated || !data.passwordRequired) && (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={globalLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingFeedback message={"Loading file details..."} />}
|
||||
{!isLoading && error && <ErrorFeedback message={error.errors?.message} />}
|
||||
{!isLoading && !error && data && (
|
||||
<FileDetails
|
||||
data={data.file}
|
||||
hash={passwordStorage?.[passwordParent as string] || ""}
|
||||
/>
|
||||
<FileDetails
|
||||
data={data.file}
|
||||
hash={passwordStorage?.[passwordParent as string] || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -95,6 +147,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
`http://localhost:5000/api/files/${id}`,
|
||||
);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (passwordParent) {
|
||||
return {
|
||||
props: {
|
||||
|
||||
+93
-26
@@ -1,10 +1,16 @@
|
||||
import useSWR from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import fetcher, { buildNextKey } from "@utils/swrFetch";
|
||||
import { ErrorResponse, FilesResponse, TFile } from "@/types/googleapis";
|
||||
import Breadcrumb from "@/components/Breadcrumb";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import MarkdownRender from "@/components/utility/MarkdownRender";
|
||||
import config from "@config/site.config";
|
||||
import GridLayout from "@components/layout/Files/GridLayout";
|
||||
@@ -26,15 +32,19 @@ export default function Folder({ passwordParent }: Props) {
|
||||
const { id } = router.query;
|
||||
|
||||
const [data, setData] = useState<FilesResponse>();
|
||||
const [dataLoading, setDataLoading] = useState<boolean>(true);
|
||||
const [isReadmeExists, setIsReadmeExists] = useState<boolean>(false);
|
||||
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
|
||||
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
|
||||
|
||||
const [passwordStorage] = useLocalStorage<{
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
|
||||
const [password, setPassword] = useState<{ [p: string]: string }>(
|
||||
passwordStorage,
|
||||
);
|
||||
|
||||
const getNextKey = buildNextKey(`/api/files/${id}`);
|
||||
const {
|
||||
data: swrData,
|
||||
@@ -42,25 +52,47 @@ export default function Folder({ passwordParent }: Props) {
|
||||
isLoading,
|
||||
size,
|
||||
setSize,
|
||||
} = 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),
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWRInfinite<FilesResponse, ErrorResponse>(
|
||||
getNextKey,
|
||||
(url, headers) =>
|
||||
axios
|
||||
.get<FilesResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
password?.[passwordParent as string] ||
|
||||
password?.[id as string] ||
|
||||
passwordStorage?.[passwordParent as string] ||
|
||||
passwordStorage?.[id as string] ||
|
||||
""
|
||||
}`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: readmeData,
|
||||
error: readmeError,
|
||||
isLoading: readmeLoading,
|
||||
} = useSWR(`/api/readme/${id}`, fetcher);
|
||||
} = useSWR(`/api/readme/${id}`, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
const isLoadingInitialData = !swrData && !error;
|
||||
const isLoadingMore =
|
||||
@@ -74,7 +106,7 @@ export default function Folder({ passwordParent }: Props) {
|
||||
typeof swrData[swrData.length - 1]?.nextPageToken === "undefined");
|
||||
|
||||
useEffect(() => {
|
||||
setDataLoading(true);
|
||||
setGlobalLoading(true);
|
||||
const files: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
swrData?.flatMap((item: FilesResponse) => item.files);
|
||||
const folders: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
@@ -85,28 +117,58 @@ export default function Folder({ passwordParent }: Props) {
|
||||
folders: folders || [],
|
||||
};
|
||||
setData(newData);
|
||||
setDataLoading(false);
|
||||
if (newData.readmeExists) setIsReadmeExists(true);
|
||||
setGlobalLoading(false);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [swrData, error, isLoading, size]);
|
||||
}, [swrData, error, isLoading, size, isValidating, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isValidating) {
|
||||
setGlobalLoading(false);
|
||||
} else {
|
||||
setGlobalLoading(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading, isValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
|
||||
const inputPassCallback = useCallback(
|
||||
(data: { [p: string]: string }) => {
|
||||
setGlobalLoading(true);
|
||||
setPasswordStorage(data);
|
||||
setPassword(data);
|
||||
},
|
||||
[setPasswordStorage],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={dataLoading}
|
||||
isLoading={globalLoading}
|
||||
/>
|
||||
<SwitchLayout setLayoutStyle={setLayoutStyle} />
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingFeedback message={"Loading file..."} />}
|
||||
{!isLoading && error && <ErrorFeedback message={error.errors?.message} />}
|
||||
{!isLoading && !error && data && (
|
||||
{globalLoading && <LoadingFeedback message={"Loading file..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message} />
|
||||
)}
|
||||
{!globalLoading && !error && data && (
|
||||
<>
|
||||
{data.passwordRequired && !data.passwordValidated && (
|
||||
<Password folderId={id as string} />
|
||||
<Password
|
||||
folderId={(passwordParent as string) || (id as string)}
|
||||
inputCallback={inputPassCallback}
|
||||
/>
|
||||
)}
|
||||
{(data.passwordValidated || !data.passwordRequired) && (
|
||||
<>
|
||||
@@ -178,6 +240,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
`http://localhost:5000/api/files/${id}`,
|
||||
);
|
||||
|
||||
context.res.setHeader(
|
||||
"Cache-Control",
|
||||
"public, s-maxage=10, stale-while-revalidate=59",
|
||||
);
|
||||
|
||||
if (passwordParent) {
|
||||
return {
|
||||
props: {
|
||||
|
||||
+71
-22
@@ -4,7 +4,7 @@ import fetcher, { buildNextKey } from "@utils/swrFetch";
|
||||
import { ErrorResponse, FilesResponse, TFile } from "@/types/googleapis";
|
||||
import Breadcrumb from "@/components/Breadcrumb";
|
||||
import { drive_v3 } from "googleapis";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import MarkdownRender from "@/components/utility/MarkdownRender";
|
||||
import config from "@config/site.config";
|
||||
import GridLayout from "@components/layout/Files/GridLayout";
|
||||
@@ -13,20 +13,22 @@ 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>();
|
||||
const [dataLoading, setDataLoading] = useState<boolean>(true);
|
||||
const [isReadmeExists, setIsReadmeExists] = useState<boolean>(false);
|
||||
const [renderStyle] = useLocalStorage<"grid" | "list">("renderStyle", "grid");
|
||||
const [layoutStyle, setLayoutStyle] = useState<"grid" | "list">(renderStyle);
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true);
|
||||
|
||||
const [passwordStorage, setPasswordStorage] = useLocalStorage<{
|
||||
[key: string]: string;
|
||||
}>("passwordStorage", {});
|
||||
const [password, setPassword] = useState<{ [p: string]: string }>(
|
||||
passwordStorage,
|
||||
);
|
||||
|
||||
const getNextKey = buildNextKey("/api/files/");
|
||||
const {
|
||||
@@ -35,24 +37,41 @@ export default function Home() {
|
||||
isLoading,
|
||||
size,
|
||||
setSize,
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useSWRInfinite<FilesResponse, ErrorResponse>(getNextKey, (url, headers) =>
|
||||
axios
|
||||
.get<FilesResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
passwordStorage?.[config.files.rootFolder] || ""
|
||||
}`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
} = useSWRInfinite<FilesResponse, ErrorResponse>(
|
||||
getNextKey,
|
||||
(url, headers) =>
|
||||
axios
|
||||
.get<FilesResponse>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
passwordStorage?.[config.files.rootFolder] || ""
|
||||
}`,
|
||||
...headers,
|
||||
},
|
||||
})
|
||||
.then((res) => res.data),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
revalidateIfStale: true,
|
||||
},
|
||||
);
|
||||
const {
|
||||
data: readmeData,
|
||||
error: readmeError,
|
||||
isLoading: readmeLoading,
|
||||
} = useSWR("/api/readme/", fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenOffline: false,
|
||||
refreshWhenHidden: false,
|
||||
refreshInterval: 0,
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
@@ -68,7 +87,7 @@ export default function Home() {
|
||||
typeof swrData[swrData.length - 1]?.nextPageToken === "undefined");
|
||||
|
||||
useEffect(() => {
|
||||
setDataLoading(true);
|
||||
setGlobalLoading(true);
|
||||
const files: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
swrData?.flatMap((item: FilesResponse) => item.files);
|
||||
const folders: (TFile | drive_v3.Schema$File)[] | undefined =
|
||||
@@ -79,31 +98,61 @@ export default function Home() {
|
||||
folders: folders || [],
|
||||
};
|
||||
setData(newData);
|
||||
setDataLoading(false);
|
||||
if (newData.readmeExists) setIsReadmeExists(true);
|
||||
setGlobalLoading(false);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [swrData, error, isLoading, size]);
|
||||
}, [swrData, error, isLoading, size, isValidating, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isValidating) {
|
||||
setGlobalLoading(false);
|
||||
} else {
|
||||
setGlobalLoading(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading, isValidating]);
|
||||
|
||||
useEffect(() => {
|
||||
mutate(swrData, {
|
||||
revalidate: true,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [password]);
|
||||
|
||||
const inputPassCallback = useCallback(
|
||||
(data: { [p: string]: string }) => {
|
||||
setGlobalLoading(true);
|
||||
setPasswordStorage(data);
|
||||
setPassword(data);
|
||||
},
|
||||
[setPasswordStorage],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx-auto flex max-w-screen-xl flex-col gap-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Breadcrumb
|
||||
data={data?.parents || []}
|
||||
isLoading={dataLoading}
|
||||
isLoading={globalLoading}
|
||||
/>
|
||||
<SwitchLayout setLayoutStyle={setLayoutStyle} />
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingFeedback message={"Loading file..."} />}
|
||||
{!isLoading && error && (
|
||||
{globalLoading && <LoadingFeedback message={"Loading file..."} />}
|
||||
{!globalLoading && error && (
|
||||
<ErrorFeedback message={error.errors?.message || "Unknown error"} />
|
||||
)}
|
||||
{!isLoading && !error && data && (
|
||||
{!globalLoading && !error && data && (
|
||||
<>
|
||||
{/* If the root folder have password and the password isn't validated, show password input */}
|
||||
{data.passwordRequired && !data.passwordValidated && (
|
||||
<Password folderId={config.files.rootFolder} />
|
||||
<Password
|
||||
folderId={config.files.rootFolder}
|
||||
inputCallback={inputPassCallback}
|
||||
/>
|
||||
)}
|
||||
{/* If password is validated or the root folder doesn't require password, show the files */}
|
||||
{(data.passwordValidated || !data.passwordRequired) && (
|
||||
<>
|
||||
{isReadmeExists && config.readme.position === "start" && (
|
||||
|
||||
@@ -106,21 +106,21 @@ html, body {
|
||||
@apply border-blue-400 hover:border-blue-500 active:border-blue-600;
|
||||
@apply bg-blue-500 hover:bg-blue-700 active:bg-blue-400;
|
||||
@apply dark:border-blue-500 dark:hover:border-blue-600 dark:active:border-blue-700;
|
||||
@apply dark:bg-blue-500 dark:hover:bg-blue-600 dark:active:bg-blue-600;
|
||||
@apply dark:bg-blue-500 dark:hover:bg-blue-600 dark:active:bg-blue-700;
|
||||
}
|
||||
button.secondary{
|
||||
@apply text-zinc-100 dark:text-zinc-100;
|
||||
@apply border-zinc-400 hover:border-zinc-500 active:border-zinc-600;
|
||||
@apply bg-zinc-500 hover:bg-zinc-600 active:bg-zinc-400;
|
||||
@apply bg-zinc-500 hover:bg-zinc-700 active:bg-zinc-400;
|
||||
@apply dark:border-zinc-500 dark:hover:border-zinc-600 dark:active:border-zinc-700;
|
||||
@apply dark:bg-zinc-500 dark:hover:bg-zinc-400 dark:active:bg-zinc-600;
|
||||
@apply dark:bg-zinc-500 dark:hover:bg-zinc-600 dark:active:bg-zinc-700;
|
||||
}
|
||||
button.danger {
|
||||
@apply text-zinc-100 dark:text-zinc-100;
|
||||
@apply border-red-400 hover:border-red-500 active:border-red-600;
|
||||
@apply bg-red-500 hover:bg-red-700 active:bg-red-400;
|
||||
@apply dark:border-red-500 dark:hover:border-red-600 dark:active:border-red-700;
|
||||
@apply dark:bg-red-500 dark:hover:bg-red-600 dark:active:bg-red-600;
|
||||
@apply dark:bg-red-500 dark:hover:bg-red-600 dark:active:bg-red-700;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -150,7 +150,7 @@ html, body {
|
||||
}
|
||||
|
||||
div.banner {
|
||||
@apply rounded-lg p-2 tablet:p-4 my-2 tablet:my-4;
|
||||
@apply rounded-lg p-2 tablet:p-4 my-2;
|
||||
@apply border border-zinc-400 dark:border-zinc-700;
|
||||
@apply drop-shadow;
|
||||
@apply bg-zinc-100 dark:bg-zinc-900;
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function _validateFolderPassword(
|
||||
|
||||
export async function validateProtected(
|
||||
fileId: string | TFileParent[],
|
||||
passwordHash: string,
|
||||
passwordHash?: string,
|
||||
): Promise<{ isProtected: boolean; valid?: boolean; protectedId?: string }> {
|
||||
const fetchPassword = await drive.files.list({
|
||||
q: `name = '.password' and 'me' in owners and trashed = false`,
|
||||
|
||||
@@ -7,3 +7,9 @@ export function hashToken(text: string): string {
|
||||
export function verifyHash(text: string, hash: string): boolean {
|
||||
return hashToken(text) === hash;
|
||||
}
|
||||
|
||||
// It's not a good idea to use this.
|
||||
// Change this to jwt or something else.
|
||||
export function reverseString(str: string): string {
|
||||
return str.split("").reverse().join("");
|
||||
}
|
||||
|
||||
@@ -3239,6 +3239,11 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next-seo@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/next-seo/-/next-seo-6.0.0.tgz#4568dc61a44dbdf5fe5ff44156cd0ff8804889a2"
|
||||
integrity sha512-jKKt1p1z4otMA28AyeoAONixVjdYmgFCWwpEFtu+DwRHQDllVX3RjtyXbuCQiUZEfQ9rFPBpAI90vDeLZlMBdg==
|
||||
|
||||
next@13.3.0:
|
||||
version "13.3.0"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.3.0.tgz#40632d303d74fc8521faa0a5bf4a033a392749b1"
|
||||
|
||||
Reference in New Issue
Block a user