1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-11-05 15:30:14 +01: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"], attributes: ["expiresAt"],
orders: ["ASC"], orders: ["ASC"],
}, },
{
key: "enabled",
type: "key",
attributes: ["enabled"],
orders: ["ASC"],
},
], ],
}, },
{ {

View File

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

14
package-lock.json generated
View File

@ -22,6 +22,7 @@
"jose": "^4.8.1", "jose": "^4.8.1",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jszip": "^3.9.1", "jszip": "^3.9.1",
"moment": "^2.29.3",
"next": "12.1.5", "next": "12.1.5",
"next-pwa": "^5.5.2", "next-pwa": "^5.5.2",
"node-appwrite": "^5.1.0", "node-appwrite": "^5.1.0",
@ -5690,6 +5691,14 @@
"node": ">=10" "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": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -11991,6 +12000,11 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true "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": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

View File

@ -25,6 +25,7 @@
"jose": "^4.8.1", "jose": "^4.8.1",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"jszip": "^3.9.1", "jszip": "^3.9.1",
"moment": "^2.29.3",
"next": "12.1.5", "next": "12.1.5",
"next-pwa": "^5.5.2", "next-pwa": "^5.5.2",
"node-appwrite": "^5.1.0", "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 Image from "next/image";
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import headerStyle from "../../styles/header.style"; import headerStyle from "../../styles/header.style";
import aw from "../../utils/appwrite.util";
import { IsSignedInContext } from "../../utils/auth.util"; import { IsSignedInContext } from "../../utils/auth.util";
import { useConfig } from "../../utils/config.util"; import { useConfig } from "../../utils/config.util";
import ToggleThemeButton from "./ToggleThemeButton"; import ActionAvatar from "./ActionAvatar";
type Link = { type Link = {
link?: string; link?: string;
@ -36,26 +35,21 @@ const Header = () => {
link: "/upload", link: "/upload",
label: "Upload", label: "Upload",
}, },
{
label: "Sign out",
action: async () => {
await aw.account.deleteSession("current");
window.location.reload();
},
},
]; ];
const unauthenticatedLinks: Link[] | undefined = [ const unauthenticatedLinks: Link[] | undefined = [
{
link: "/",
label: "Home",
},
{ {
link: "/auth/signIn", link: "/auth/signIn",
label: "Sign in", label: "Sign in",
}, },
]; ];
if (!config.DISABLE_HOME_PAGE)
unauthenticatedLinks.unshift({
link: "/",
label: "Home",
});
if (!config.DISABLE_REGISTRATION) if (!config.DISABLE_REGISTRATION)
unauthenticatedLinks.push({ unauthenticatedLinks.push({
link: "/auth/signUp", link: "/auth/signUp",
@ -67,11 +61,11 @@ const Header = () => {
const items = links.map((link) => { const items = links.map((link) => {
if (link) { if (link) {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
// useEffect(() => { useEffect(() => {
// if (window.location.pathname == link.link) { if (window.location.pathname == link.link) {
// setActive(link.link); setActive(link.link);
// } }
// }); });
return ( return (
<NextLink <NextLink
key={link.label} key={link.label}
@ -104,9 +98,8 @@ const Header = () => {
<Group spacing={5} className={classes.links}> <Group spacing={5} className={classes.links}>
{items} {items}
<Space w={5} /> <Space w={5} />
<ToggleThemeButton /> {isSignedIn && <ActionAvatar />}
</Group> </Group>
<Burger <Burger
opened={opened} opened={opened}
onClick={() => toggleOpened()} onClick={() => toggleOpened()}

View File

@ -32,7 +32,10 @@ const CreateUploadModalBody = ({
const modals = useModals(); const modals = useModals();
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
link: yup.string().required().min(2).max(50), 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), password: yup.string().min(3).max(100),
maxVisitors: yup.number().min(1), maxVisitors: yup.number().min(1),
}); });

View File

@ -42,6 +42,7 @@ const FileList = ({
height={30} height={30}
alt={file.name} alt={file.name}
objectFit="cover" objectFit="cover"
style={{ borderRadius: 3 }}
src={`data:image/png;base64,${new Buffer(file.preview).toString( src={`data:image/png;base64,${new Buffer(file.preview).toString(
"base64" "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,6 +7,8 @@ import * as jose from "jose";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const shareId = req.query.shareId as string; const shareId = req.query.shareId as string;
if (req.method == "POST") {
const fileList: AppwriteFileWithPreview[] = []; const fileList: AppwriteFileWithPreview[] = [];
const hashedPassword = req.cookies[`${shareId}-password`]; const hashedPassword = req.cookies[`${shareId}-password`];
@ -54,8 +56,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
`${shareId}-password=${hashedPassword}; Path=/share/${shareId}; max-age=3600; HttpOnly` `${shareId}-password=${hashedPassword}; Path=/share/${shareId}; max-age=3600; HttpOnly`
); );
res.status(200).json(fileList); res.status(200).json(fileList);
} else if (req.method == "DELETE") {
awServer.database.updateDocument("shares", shareId, {
enabled: false,
});
}
}; };
// Util functions
const hasUserAccess = (jwt: string, shareDocument: ShareDocument) => { const hasUserAccess = (jwt: string, shareDocument: ShareDocument) => {
if (shareDocument.users?.length == 0) return true; if (shareDocument.users?.length == 0) return true;
try { try {
@ -79,5 +87,4 @@ const addVisitorCount = async (shareId: string) => {
awServer.database.updateDocument("shares", shareId, currentDocument); awServer.database.updateDocument("shares", shareId, currentDocument);
}; };
export default handler; export default handler;

View File

@ -5,6 +5,11 @@ const get = async (shareId: string, password?: string) => {
return (await axios.post(`/api/share/${shareId}`, { password })) return (await axios.post(`/api/share/${shareId}`, { password }))
.data as AppwriteFileWithPreview[]; .data as AppwriteFileWithPreview[];
}; };
const remove = async (shareId: string) => {
await axios.delete(`/api/share/${shareId}`);
};
const isIdAlreadyInUse = async (shareId: string) => { const isIdAlreadyInUse = async (shareId: string) => {
return (await axios.get(`/api/share/${shareId}/exists`)).data return (await axios.get(`/api/share/${shareId}/exists`)).data
.exists as boolean; .exists as boolean;
@ -21,6 +26,7 @@ const authenticateWithPassword = async (shareId: string, password?: string) => {
export default { export default {
get, get,
remove,
authenticateWithPassword, authenticateWithPassword,
isIdAlreadyInUse, isIdAlreadyInUse,
doesUserExist, doesUserExist,