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:
parent
80f055899c
commit
3cb7285e8f
@ -65,6 +65,12 @@ export default [
|
||||
attributes: ["expiresAt"],
|
||||
orders: ["ASC"],
|
||||
},
|
||||
{
|
||||
key: "enabled",
|
||||
type: "key",
|
||||
attributes: ["enabled"],
|
||||
orders: ["ASC"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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
14
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
43
src/components/navBar/ActionAvatar.tsx
Normal file
43
src/components/navBar/ActionAvatar.tsx
Normal 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;
|
@ -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()}
|
||||
|
@ -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),
|
||||
});
|
||||
|
@ -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"
|
||||
)}`}
|
||||
|
132
src/pages/account/shares.tsx
Normal file
132
src/pages/account/shares.tsx
Normal 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;
|
@ -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;
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user