1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-10-01 00:50:10 +02:00

Add User Info Page

This commit is contained in:
Elias Schneider 2022-05-11 13:50:28 +02:00
parent 80f055899c
commit 3cb7285e8f
No known key found for this signature in database
GPG Key ID: D5EC1C72D93244FD
11 changed files with 274 additions and 68 deletions

View File

@ -65,6 +65,12 @@ export default [
attributes: ["expiresAt"],
orders: ["ASC"],
},
{
key: "enabled",
type: "key",
attributes: ["enabled"],
orders: ["ASC"],
},
],
},
{

View File

@ -63,7 +63,7 @@ module.exports = async function (req, res) {
users: userIds,
createdAt: Date.now(),
expiresAt: expiration,
});
}, [`user:${userId}`]);
res.json({
id: payload.id,

14
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 (
<Menu
control={
<ActionIcon>
<Avatar size={28} radius="xl" />
</ActionIcon>
}
>
<Menu.Label>My account</Menu.Label>
<Menu.Item
component={NextLink}
href="/account/shares"
icon={<Link size={14} />}
>
Shares
</Menu.Item>
{/* <Menu.Item
component={NextLink}
href="/account/shares"
icon={<Settings size={14} />}
>
Settings
</Menu.Item> */}
<Menu.Item
onClick={async () => {
await aw.account.deleteSession("current");
window.location.reload();
}}
icon={<DoorExit size={14} />}
>
Sign out
</Menu.Item>
</Menu>
);
};
export default ActionAvatar;

View File

@ -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 (
<NextLink
key={link.label}
@ -104,9 +98,8 @@ const Header = () => {
<Group spacing={5} className={classes.links}>
{items}
<Space w={5} />
<ToggleThemeButton />
{isSignedIn && <ActionAvatar />}
</Group>
<Burger
opened={opened}
onClick={() => toggleOpened()}

View File

@ -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),
});

View File

@ -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"
)}`}

View File

@ -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<ShareDocument[]>();
useEffect(() => {
aw.database
.listDocuments<ShareDocument>(
"shares",
[Query.equal("enabled", true)],
100
)
.then((res) => setShares(res.documents));
}, []);
if (!shares) return <LoadingOverlay visible />;
return (
<>
<Meta title="My shares" />
<Title mb={30} order={3}>
My shares
</Title>
{shares.length == 0 ? (
<Center style={{ height: "70vh" }}>
<Group direction="column" align="center" spacing={10}>
<Title order={3}>It's empty here 👀</Title>
<Text>You don't have any shares.</Text>
<Space h={5} />
<Button component={NextLink} href="/upload" variant="light">
Create one
</Button>
</Group>
</Center>
) : (
<Table>
<thead>
<tr>
<th>Name</th>
<th>Visitors</th>
<th>Security enabled</th>
<th>Email</th>
<th>Expires at</th>
<th></th>
</tr>
</thead>
<tbody>
{shares.map((share) => (
<tr key={share.$id}>
<td>{share.$id}</td>
<td>{share.visitorCount}</td>
<td>{share.securityID ? "Yes" : "No"}</td>
<td>{share.users!.length > 0 ? "Yes" : "No"}</td>
<td>{moment(share.expiresAt).format("MMMM DD YYYY, HH:mm")}</td>
<td>
<Group position="right">
<ActionIcon
color="victoria"
variant="light"
size={25}
onClick={() => {
clipboard.copy(
`${window.location.origin}/share/${share.$id}`
);
toast.success("Your link was copied to the keyboard.");
}}
>
<Link />
</ActionIcon>
<ActionIcon
color="red"
variant="light"
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete share ${share.$id}`,
children: (
<Text size="sm">
Do you really want to delete this share?
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
onConfirm: () => {
shareService.remove(share.$id);
setShares(
shares.filter((item) => item.$id !== share.$id)
);
},
});
}}
>
<Trash />
</ActionIcon>
</Group>
</td>
</tr>
))}
</tbody>
</Table>
)}
</>
);
};
export default MyShares;

View File

@ -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<ShareDocument>(
"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<ShareDocument>(
"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;

View File

@ -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,