diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index c9180e6..c7f9609 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -15,10 +15,10 @@ body: id: version attributes: label: Version - description: What version of Zipline are you using? + description: What version (or docker image) of Zipline are you using? options: + - latest (ghcr.io/diced/zipline or ghcr.io/diced/zipline:latest) - upstream (ghcr.io/diced/zipline:trunk) - - latest (ghcr.io/diced/zipline:latest) - other (provide version in additional info) validations: required: true diff --git a/src/components/File/FileModal.tsx b/src/components/File/FileModal.tsx index ed57719..b583657 100644 --- a/src/components/File/FileModal.tsx +++ b/src/components/File/FileModal.tsx @@ -49,6 +49,7 @@ export default function FileModal({ reducedActions = false, exifEnabled, compress, + otherUser = false, }: { open: boolean; setOpen: (open: boolean) => void; @@ -58,6 +59,7 @@ export default function FileModal({ reducedActions?: boolean; exifEnabled?: boolean; compress: boolean; + otherUser: boolean; }) { const deleteFile = useFileDelete(); const favoriteFile = useFileFavorite(); @@ -276,7 +278,7 @@ export default function FileModal({ )} - {reducedActions ? null : inFolder && !folders.isLoading ? ( + {reducedActions || otherUser ? null : inFolder && !folders.isLoading ? ( f.id === file.folderId)?.name ?? ''}"`} > diff --git a/src/components/File/index.tsx b/src/components/File/index.tsx index 8ab6a22..7670c44 100644 --- a/src/components/File/index.tsx +++ b/src/components/File/index.tsx @@ -32,9 +32,10 @@ export default function File({ image, disableMediaPreview, exifEnabled, - refreshImages, + refreshImages = undefined, reducedActions = false, onDash, + otherUser = false, }) { const [open, setOpen] = useState(false); const deleteFile = useFileDelete(); @@ -44,7 +45,7 @@ export default function File({ const folders = useFolders(); const refresh = () => { - refreshImages(); + if (!otherUser) refreshImages(); folders.refetch(); }; @@ -59,6 +60,7 @@ export default function File({ reducedActions={reducedActions} exifEnabled={exifEnabled} compress={onDash} + otherUser={otherUser} /> setOpen(true)}> diff --git a/src/components/pages/Dashboard/index.tsx b/src/components/pages/Dashboard/index.tsx index 8de2918..bcbfeff 100644 --- a/src/components/pages/Dashboard/index.tsx +++ b/src/components/pages/Dashboard/index.tsx @@ -126,6 +126,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress } reducedActions={false} exifEnabled={exifEnabled} compress={compress} + otherUser={false} /> )} diff --git a/src/components/pages/Users/UserFiles.tsx b/src/components/pages/Users/UserFiles.tsx new file mode 100644 index 0000000..0b4cc77 --- /dev/null +++ b/src/components/pages/Users/UserFiles.tsx @@ -0,0 +1,82 @@ +import { ActionIcon, Button, Center, Group, SimpleGrid, Title } from '@mantine/core'; +import { File } from '@prisma/client'; +import { IconArrowLeft, IconFile } from '@tabler/icons-react'; +import FileComponent from 'components/File'; +import MutedText from 'components/MutedText'; +import useFetch from 'hooks/useFetch'; +import { userSelector } from 'lib/recoil/user'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +type UserFiles = { + id: number; + username: string; + files?: File[]; + error?: unknown; +}; + +export default function UserFiles({ userId, disableMediaPreview, exifEnabled, compress }) { + const [currentUser, viewUser] = useState({ id: 0, username: 'user' }); + const [self] = useRecoilState(userSelector); + + const { push } = useRouter(); + + useEffect(() => { + if (self.id == userId) push('/dashboard/files'); + (async () => { + const user: UserFiles = await useFetch(`/api/user/${userId}`); + if (!user.error) { + viewUser(user); + } else { + push('/dashboard'); + } + })(); + }, [userId]); + + if (!currentUser.files || currentUser.files.length === 0) { + return ( +
+ +
+ +
+
+ Nothing here + + {currentUser.username} seems to have not uploaded any files... yet + +
+ +
+
+ ); + } + + return ( + <> + + push('/dashboard/users')} color='primary'> + + + {currentUser.username}'s Files + + + + {currentUser.files.map((file) => ( +
+ +
+ ))} +
+ + ); +} diff --git a/src/components/pages/Users/index.tsx b/src/components/pages/Users/index.tsx index 993c6d9..443d0b7 100644 --- a/src/components/pages/Users/index.tsx +++ b/src/components/pages/Users/index.tsx @@ -6,6 +6,7 @@ import type { User } from '@prisma/client'; import { IconClipboardCopy, IconEdit, + IconExternalLink, IconGridDots, IconList, IconUserExclamation, @@ -116,6 +117,10 @@ export default function Users() { } }; + const openUser = async (user) => { + await router.push(`/dashboard/users/${user.id}`); + }; + useEffect(() => { updateUsers(); }, []); @@ -181,6 +186,13 @@ export default function Users() {
+ {(!self.superAdmin && user.superAdmin) || (self.superAdmin && user.superAdmin) ? null : ( + + openUser(user)}> + + + + )} ), }, diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index 651bc08..5767f3a 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -14,6 +14,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { where: { id: Number(id), }, + include: { + files: true, + Folder: true, + }, }); if (!target) return res.notFound('user not found'); @@ -175,6 +179,10 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { } else { delete target.password; + if (user.superAdmin && target.superAdmin) delete target.files; + if (user.administrator && !user.superAdmin && (target.administrator || target.superAdmin)) + delete target.files; + return res.json(target); } } diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index d8ce4b8..ab84430 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -31,15 +31,46 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { } else { if (!req.body.id) return res.badRequest('no file id'); - const file = await prisma.file.delete({ + let file = await prisma.file.findFirst({ where: { id: req.body.id, + userId: user.id, + }, + include: { + user: { + select: { + administrator: true, + superAdmin: true, + username: true, + id: true, + }, + }, + }, + }); + + if (!file && (!user.administrator || !user.superAdmin)) return res.notFound('file not found'); + + file = await prisma.file.delete({ + where: { + id: req.body.id, + }, + include: { + user: { + select: { + administrator: true, + superAdmin: true, + username: true, + id: true, + }, + }, }, }); await datasource.delete(file.name); - logger.info(`User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id})`); + logger.info( + `User ${user.username} (${user.id}) deleted an image ${file.name} (${file.id}) owned by ${file.user.username} (${file.user.id})` + ); // @ts-ignore if (file.password) file.password = true; @@ -51,14 +82,33 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { let file; - if (req.body.favorite !== null) + if (req.body.favorite !== null) { + file = await prisma.file.findFirst({ + where: { + id: req.body.id, + userId: user.id, + }, + include: { + user: { + select: { + administrator: true, + superAdmin: true, + username: true, + id: true, + }, + }, + }, + }); + + if (!file && (!user.administrator || !user.superAdmin)) return res.notFound('file not found'); + file = await prisma.file.update({ where: { id: req.body.id }, data: { favorite: req.body.favorite, }, }); - + } // @ts-ignore if (file.password) file.password = true; return res.json(file); diff --git a/src/pages/dashboard/users/[id].tsx b/src/pages/dashboard/users/[id].tsx new file mode 100644 index 0000000..03fa1bf --- /dev/null +++ b/src/pages/dashboard/users/[id].tsx @@ -0,0 +1,42 @@ +import { LoadingOverlay } from '@mantine/core'; +import Layout from 'components/Layout'; +import UserFiles from 'components/pages/Users/UserFiles'; +import useLogin from 'hooks/useLogin'; +import Head from 'next/head'; +import { getServerSideProps as middlewareProps } from 'middleware/getServerSideProps'; +import { GetServerSideProps } from 'next'; + +export default function UsersId(props) { + const { loading } = useLogin(); + + if (loading) return ; + + const title = `${props.title} - User - ${props.userId}`; + return ( + <> + + {title} + + + + + + ); +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { id } = context.params as { id: string }; + // @ts-ignore + const { props } = await middlewareProps(context); + return { + props: { + userId: id, + ...props, + }, + }; +}; diff --git a/src/pages/dashboard/users.tsx b/src/pages/dashboard/users/index.tsx similarity index 93% rename from src/pages/dashboard/users.tsx rename to src/pages/dashboard/users/index.tsx index 676b457..971df81 100644 --- a/src/pages/dashboard/users.tsx +++ b/src/pages/dashboard/users/index.tsx @@ -10,7 +10,7 @@ export default function UsersPage(props) { if (loading) return ; - const title = `${props.title} - User`; + const title = `${props.title} - Users`; return ( <> diff --git a/src/server/util.ts b/src/server/util.ts index 9234d7b..dec7af0 100644 --- a/src/server/util.ts +++ b/src/server/util.ts @@ -61,6 +61,7 @@ export async function migrations() { logger.error( `Unable to connect to database \`${process.env.DATABASE_URL}\`, check your database connection` ); + logger.debug(error); } else { logger.error('Failed to migrate database... exiting...'); logger.error(error);