Protected folder and files implemented.

This commit is contained in:
mbaharip
2023-04-27 08:11:54 +07:00
parent 3d49d89183
commit 40712d9f3c
17 changed files with 423 additions and 172 deletions
+1
View File
@@ -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",
+42 -38
View File
@@ -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>
);
}
+4 -1
View File
@@ -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>
+7 -1
View File
@@ -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 ${
+21 -5
View File
@@ -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>
+1 -1
View File
@@ -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: {
+7 -5
View File
@@ -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,
+11 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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" && (
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -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`,
+6
View File
@@ -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("");
}
+5
View File
@@ -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"