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 (
+
+ );
+};
+
+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.
+
+
+
+
+ ) : (
+
+
+
+ Name |
+ Visitors |
+ Security enabled |
+ Email |
+ Expires at |
+ |
+
+
+
+ {shares.map((share) => (
+
+ {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,