Merge branch 'trunk' into feature/oauth-authentik
This commit is contained in:
commit
a12b18c546
11 changed files with 210 additions and 10 deletions
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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({
|
|||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
{reducedActions || otherUser ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md' onClick={() => setOpen(true)}>
|
||||
|
|
|
@ -126,6 +126,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
|||
reducedActions={false}
|
||||
exifEnabled={exifEnabled}
|
||||
compress={compress}
|
||||
otherUser={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
82
src/components/pages/Users/UserFiles.tsx
Normal file
82
src/components/pages/Users/UserFiles.tsx
Normal file
|
@ -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<UserFiles>({ 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 (
|
||||
<Center sx={{ flexDirection: 'column' }}>
|
||||
<Group>
|
||||
<div>
|
||||
<IconFile size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
<MutedText size='md'>
|
||||
{currentUser.username} seems to have not uploaded any files... yet
|
||||
</MutedText>
|
||||
</div>
|
||||
<Button size='md' onClick={() => push('/dashboard/users')}>
|
||||
Head back?
|
||||
</Button>
|
||||
</Group>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group mb='md'>
|
||||
<ActionIcon size='lg' onClick={() => push('/dashboard/users')} color='primary'>
|
||||
<IconArrowLeft />
|
||||
</ActionIcon>
|
||||
<Title>{currentUser.username}'s Files</Title>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{currentUser.files.map((file) => (
|
||||
<div key={file.id}>
|
||||
<FileComponent
|
||||
image={file}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
onDash={compress}
|
||||
otherUser={true}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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() {
|
|||
<IconEdit size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{(!self.superAdmin && user.superAdmin) || (self.superAdmin && user.superAdmin) ? null : (
|
||||
<Tooltip label='Open user'>
|
||||
<ActionIcon color='cyan' onClick={() => openUser(user)}>
|
||||
<IconExternalLink size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
42
src/pages/dashboard/users/[id].tsx
Normal file
42
src/pages/dashboard/users/[id].tsx
Normal file
|
@ -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 <LoadingOverlay visible={loading} />;
|
||||
|
||||
const title = `${props.title} - User - ${props.userId}`;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<Layout props={props}>
|
||||
<UserFiles
|
||||
userId={props.userId}
|
||||
disableMediaPreview={props.disable_media_preview}
|
||||
exifEnabled={props.exif_enabled}
|
||||
compress={props.compress}
|
||||
/>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -10,7 +10,7 @@ export default function UsersPage(props) {
|
|||
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
const title = `${props.title} - User`;
|
||||
const title = `${props.title} - Users`;
|
||||
return (
|
||||
<>
|
||||
<Head>
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue