From 3cb7285e8f8e7fc49df935b4e0e1b1f6d253c4da Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Wed, 11 May 2022 13:50:28 +0200 Subject: [PATCH] Add User Info Page --- .setup/data/collections.ts | 6 + functions/createShare/src/index.js | 2 +- package-lock.json | 14 ++ package.json | 1 + src/components/navBar/ActionAvatar.tsx | 43 ++++++ src/components/navBar/NavBar.tsx | 33 ++--- .../share/CreateUploadModalBody.tsx | 5 +- src/components/share/FileList.tsx | 1 + src/pages/account/shares.tsx | 132 ++++++++++++++++++ src/pages/api/share/[shareId]/index.ts | 99 +++++++------ src/services/share.service.ts | 6 + 11 files changed, 274 insertions(+), 68 deletions(-) create mode 100644 src/components/navBar/ActionAvatar.tsx create mode 100644 src/pages/account/shares.tsx diff --git a/.setup/data/collections.ts b/.setup/data/collections.ts index e4d295f8..0e6bfd34 100644 --- a/.setup/data/collections.ts +++ b/.setup/data/collections.ts @@ -65,6 +65,12 @@ export default [ attributes: ["expiresAt"], orders: ["ASC"], }, + { + key: "enabled", + type: "key", + attributes: ["enabled"], + orders: ["ASC"], + }, ], }, { diff --git a/functions/createShare/src/index.js b/functions/createShare/src/index.js index b665d78c..5519106a 100644 --- a/functions/createShare/src/index.js +++ b/functions/createShare/src/index.js @@ -63,7 +63,7 @@ module.exports = async function (req, res) { users: userIds, createdAt: Date.now(), expiresAt: expiration, - }); + }, [`user:${userId}`]); res.json({ id: payload.id, diff --git a/package-lock.json b/package-lock.json index 558c9ddf..14be24b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "jose": "^4.8.1", "js-file-download": "^0.4.12", "jszip": "^3.9.1", + "moment": "^2.29.3", "next": "12.1.5", "next-pwa": "^5.5.2", "node-appwrite": "^5.1.0", @@ -5690,6 +5691,14 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11991,6 +12000,11 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, + "moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 6d6e85e1..cc118657 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "jose": "^4.8.1", "js-file-download": "^0.4.12", "jszip": "^3.9.1", + "moment": "^2.29.3", "next": "12.1.5", "next-pwa": "^5.5.2", "node-appwrite": "^5.1.0", diff --git a/src/components/navBar/ActionAvatar.tsx b/src/components/navBar/ActionAvatar.tsx new file mode 100644 index 00000000..e73182db --- /dev/null +++ b/src/components/navBar/ActionAvatar.tsx @@ -0,0 +1,43 @@ +import { ActionIcon, Avatar, Menu } from "@mantine/core"; +import { NextLink } from "@mantine/next"; +import { DoorExit, Link } from "tabler-icons-react"; +import aw from "../../utils/appwrite.util"; + +const ActionAvatar = () => { + return ( + + + + } + > + My account + } + > + Shares + + {/* } + > + Settings + */} + { + await aw.account.deleteSession("current"); + window.location.reload(); + }} + icon={} + > + Sign out + + + ); +}; + +export default ActionAvatar; diff --git a/src/components/navBar/NavBar.tsx b/src/components/navBar/NavBar.tsx index 90006e43..5dc81092 100644 --- a/src/components/navBar/NavBar.tsx +++ b/src/components/navBar/NavBar.tsx @@ -13,10 +13,9 @@ import { NextLink } from "@mantine/next"; import Image from "next/image"; import React, { useContext, useEffect, useState } from "react"; import headerStyle from "../../styles/header.style"; -import aw from "../../utils/appwrite.util"; import { IsSignedInContext } from "../../utils/auth.util"; import { useConfig } from "../../utils/config.util"; -import ToggleThemeButton from "./ToggleThemeButton"; +import ActionAvatar from "./ActionAvatar"; type Link = { link?: string; @@ -36,26 +35,21 @@ const Header = () => { link: "/upload", label: "Upload", }, - { - label: "Sign out", - action: async () => { - await aw.account.deleteSession("current"); - window.location.reload(); - }, - }, ]; const unauthenticatedLinks: Link[] | undefined = [ - { - link: "/", - label: "Home", - }, { link: "/auth/signIn", label: "Sign in", }, ]; + if (!config.DISABLE_HOME_PAGE) + unauthenticatedLinks.unshift({ + link: "/", + label: "Home", + }); + if (!config.DISABLE_REGISTRATION) unauthenticatedLinks.push({ link: "/auth/signUp", @@ -67,11 +61,11 @@ const Header = () => { const items = links.map((link) => { if (link) { // eslint-disable-next-line react-hooks/rules-of-hooks - // useEffect(() => { - // if (window.location.pathname == link.link) { - // setActive(link.link); - // } - // }); + useEffect(() => { + if (window.location.pathname == link.link) { + setActive(link.link); + } + }); return ( { {items} - + {isSignedIn && } - toggleOpened()} diff --git a/src/components/share/CreateUploadModalBody.tsx b/src/components/share/CreateUploadModalBody.tsx index fdc3caf0..fcf820cd 100644 --- a/src/components/share/CreateUploadModalBody.tsx +++ b/src/components/share/CreateUploadModalBody.tsx @@ -32,7 +32,10 @@ const CreateUploadModalBody = ({ const modals = useModals(); const validationSchema = yup.object().shape({ link: yup.string().required().min(2).max(50), - emails: mode == "email" ? yup.array().of(yup.string().email()).min(1) : yup.array(), + emails: + mode == "email" + ? yup.array().of(yup.string().email()).min(1) + : yup.array(), password: yup.string().min(3).max(100), maxVisitors: yup.number().min(1), }); diff --git a/src/components/share/FileList.tsx b/src/components/share/FileList.tsx index cdf2e0c5..8e3bfc40 100644 --- a/src/components/share/FileList.tsx +++ b/src/components/share/FileList.tsx @@ -42,6 +42,7 @@ const FileList = ({ height={30} alt={file.name} objectFit="cover" + style={{ borderRadius: 3 }} src={`data:image/png;base64,${new Buffer(file.preview).toString( "base64" )}`} diff --git a/src/pages/account/shares.tsx b/src/pages/account/shares.tsx new file mode 100644 index 00000000..85022be9 --- /dev/null +++ b/src/pages/account/shares.tsx @@ -0,0 +1,132 @@ +import { + ActionIcon, + Button, + Center, + Group, + LoadingOverlay, + Space, + Table, + Text, + Title, +} from "@mantine/core"; +import { useClipboard } from "@mantine/hooks"; +import { useModals } from "@mantine/modals"; +import { NextLink } from "@mantine/next"; +import { Query } from "appwrite"; +import moment from "moment"; +import { useEffect, useState } from "react"; +import { Link, Trash } from "tabler-icons-react"; +import Meta from "../../components/Meta"; +import shareService from "../../services/share.service"; +import { ShareDocument } from "../../types/Appwrite.type"; +import aw from "../../utils/appwrite.util"; +import toast from "../../utils/toast.util"; + +const MyShares = () => { + const modals = useModals(); + const clipboard = useClipboard(); + + const [shares, setShares] = useState(); + + useEffect(() => { + aw.database + .listDocuments( + "shares", + [Query.equal("enabled", true)], + 100 + ) + .then((res) => setShares(res.documents)); + }, []); + + if (!shares) return ; + return ( + <> + + + My shares + + {shares.length == 0 ? ( +
+ + It's empty here 👀 + You don't have any shares. + + + +
+ ) : ( + + + + + + + + + + + + + {shares.map((share) => ( + + + + + + + + + ))} + +
NameVisitorsSecurity enabledEmailExpires at
{share.$id}{share.visitorCount}{share.securityID ? "Yes" : "No"}{share.users!.length > 0 ? "Yes" : "No"}{moment(share.expiresAt).format("MMMM DD YYYY, HH:mm")} + + { + clipboard.copy( + `${window.location.origin}/share/${share.$id}` + ); + toast.success("Your link was copied to the keyboard."); + }} + > + + + { + modals.openConfirmModal({ + title: `Delete share ${share.$id}`, + children: ( + + Do you really want to delete this share? + + ), + confirmProps: { + color: "red", + }, + labels: { confirm: "Confirm", cancel: "Cancel" }, + onConfirm: () => { + shareService.remove(share.$id); + setShares( + shares.filter((item) => item.$id !== share.$id) + ); + }, + }); + }} + > + + + +
+ )} + + ); +}; + +export default MyShares; diff --git a/src/pages/api/share/[shareId]/index.ts b/src/pages/api/share/[shareId]/index.ts index f42b32f6..6394d8fa 100644 --- a/src/pages/api/share/[shareId]/index.ts +++ b/src/pages/api/share/[shareId]/index.ts @@ -7,55 +7,63 @@ import * as jose from "jose"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const shareId = req.query.shareId as string; - const fileList: AppwriteFileWithPreview[] = []; - const hashedPassword = req.cookies[`${shareId}-password`]; - let shareDocument; - try { - shareDocument = await awServer.database.getDocument( - "shares", - shareId - ); - } catch { - return res.status(404).json({ message: "not_found" }); + if (req.method == "POST") { + const fileList: AppwriteFileWithPreview[] = []; + const hashedPassword = req.cookies[`${shareId}-password`]; + + let shareDocument; + try { + shareDocument = await awServer.database.getDocument( + "shares", + shareId + ); + } catch { + return res.status(404).json({ message: "not_found" }); + } + + if (!shareExists(shareDocument)) { + return res.status(404).json({ message: "not_found" }); + } + + if (!hasUserAccess(req.cookies.aw_token, shareDocument)) { + return res.status(403).json({ message: "forbidden" }); + } + + try { + await checkSecurity(shareId, hashedPassword); + } catch (e) { + return res.status(403).json({ message: e }); + } + + addVisitorCount(shareId); + + const fileListWithoutPreview = ( + await awServer.storage.listFiles(shareId, undefined, 100) + ).files; + + for (const file of fileListWithoutPreview) { + const filePreview = await awServer.storage.getFilePreview( + shareId, + file.$id + ); + fileList.push({ ...file, preview: filePreview }); + } + + if (hashedPassword) + res.setHeader( + "Set-Cookie", + `${shareId}-password=${hashedPassword}; Path=/share/${shareId}; max-age=3600; HttpOnly` + ); + res.status(200).json(fileList); + } else if (req.method == "DELETE") { + awServer.database.updateDocument("shares", shareId, { + enabled: false, + }); } - - if (!shareExists(shareDocument)) { - return res.status(404).json({ message: "not_found" }); - } - - if (!hasUserAccess(req.cookies.aw_token, shareDocument)) { - return res.status(403).json({ message: "forbidden" }); - } - - try { - await checkSecurity(shareId, hashedPassword); - } catch (e) { - return res.status(403).json({ message: e }); - } - - addVisitorCount(shareId); - - const fileListWithoutPreview = ( - await awServer.storage.listFiles(shareId, undefined, 100) - ).files; - - for (const file of fileListWithoutPreview) { - const filePreview = await awServer.storage.getFilePreview( - shareId, - file.$id - ); - fileList.push({ ...file, preview: filePreview }); - } - - if (hashedPassword) - res.setHeader( - "Set-Cookie", - `${shareId}-password=${hashedPassword}; Path=/share/${shareId}; max-age=3600; HttpOnly` - ); - res.status(200).json(fileList); }; +// Util functions const hasUserAccess = (jwt: string, shareDocument: ShareDocument) => { if (shareDocument.users?.length == 0) return true; try { @@ -79,5 +87,4 @@ const addVisitorCount = async (shareId: string) => { awServer.database.updateDocument("shares", shareId, currentDocument); }; - export default handler; diff --git a/src/services/share.service.ts b/src/services/share.service.ts index 5cea8f11..38462932 100644 --- a/src/services/share.service.ts +++ b/src/services/share.service.ts @@ -5,6 +5,11 @@ const get = async (shareId: string, password?: string) => { return (await axios.post(`/api/share/${shareId}`, { password })) .data as AppwriteFileWithPreview[]; }; + +const remove = async (shareId: string) => { + await axios.delete(`/api/share/${shareId}`); +}; + const isIdAlreadyInUse = async (shareId: string) => { return (await axios.get(`/api/share/${shareId}/exists`)).data .exists as boolean; @@ -21,6 +26,7 @@ const authenticateWithPassword = async (shareId: string, password?: string) => { export default { get, + remove, authenticateWithPassword, isIdAlreadyInUse, doesUserExist,