mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
feat: actions when viewing other user files (#918)
This commit is contained in:
@@ -88,11 +88,13 @@ export default function FileModal({
|
|||||||
setOpen,
|
setOpen,
|
||||||
file,
|
file,
|
||||||
reduce,
|
reduce,
|
||||||
|
user,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
file?: File | null;
|
file?: File | null;
|
||||||
reduce?: boolean;
|
reduce?: boolean;
|
||||||
|
user?: string;
|
||||||
}) {
|
}) {
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
|
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
|
||||||
@@ -226,7 +228,7 @@ export default function FileModal({
|
|||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{!reduce && (
|
{!reduce && !user && (
|
||||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='md' my='xs'>
|
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='md' my='xs'>
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={4} mt='lg' mb='xs'>
|
<Title order={4} mt='lg' mb='xs'>
|
||||||
|
|||||||
@@ -69,20 +69,23 @@ export async function bulkDelete(ids: string[], setSelectedFiles: (files: File[]
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkFavorite(ids: string[]) {
|
export async function bulkFavorite(ids: string[], favorite: boolean) {
|
||||||
|
const text = favorite ? 'favorite' : 'unfavorite';
|
||||||
|
const textcaps = favorite ? 'Favorite' : 'Unfavorite';
|
||||||
|
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
centered: true,
|
centered: true,
|
||||||
title: `Favorite ${ids.length} file${ids.length === 1 ? '' : 's'}?`,
|
title: `${textcaps} ${ids.length} file${ids.length === 1 ? '' : 's'}?`,
|
||||||
children: `You are about to favorite ${ids.length} file${ids.length === 1 ? '' : 's'}.`,
|
children: `You are about to ${text} ${ids.length} file${ids.length === 1 ? '' : 's'}.`,
|
||||||
labels: {
|
labels: {
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
confirm: 'Favorite',
|
confirm: `${textcaps}`,
|
||||||
},
|
},
|
||||||
confirmProps: { color: 'yellow' },
|
confirmProps: { color: 'yellow' },
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: 'Favoriting files',
|
title: `${textcaps}ing files`,
|
||||||
message: `Favoriting ${ids.length} file${ids.length === 1 ? '' : 's'}`,
|
message: `${textcaps}ing ${ids.length} file${ids.length === 1 ? '' : 's'}`,
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
loading: true,
|
loading: true,
|
||||||
id: 'bulk-favorite',
|
id: 'bulk-favorite',
|
||||||
@@ -96,13 +99,13 @@ export async function bulkFavorite(ids: string[]) {
|
|||||||
{
|
{
|
||||||
files: ids,
|
files: ids,
|
||||||
|
|
||||||
favorite: true,
|
favorite,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
notifications.update({
|
notifications.update({
|
||||||
title: 'Error while favoriting files',
|
title: 'Error while modifying files',
|
||||||
message: error.error,
|
message: error.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <IconStarsOff size='1rem' />,
|
icon: <IconStarsOff size='1rem' />,
|
||||||
@@ -112,8 +115,8 @@ export async function bulkFavorite(ids: string[]) {
|
|||||||
});
|
});
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
notifications.update({
|
notifications.update({
|
||||||
title: 'Favorited files',
|
title: `${textcaps}d files`,
|
||||||
message: `Favorited ${data.count} file${ids.length === 1 ? '' : 's'}`,
|
message: `${textcaps}d ${data.count} file${ids.length === 1 ? '' : 's'}`,
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
icon: <IconStarsFilled size='1rem' />,
|
icon: <IconStarsFilled size='1rem' />,
|
||||||
id: 'bulk-favorite',
|
id: 'bulk-favorite',
|
||||||
|
|||||||
@@ -388,6 +388,8 @@ export default function FileTable({
|
|||||||
}
|
}
|
||||||
}, [searchField]);
|
}, [searchField]);
|
||||||
|
|
||||||
|
const unfavoriteAll = selectedFiles.every((file) => file.favorite);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FileModal
|
<FileModal
|
||||||
@@ -396,6 +398,7 @@ export default function FileTable({
|
|||||||
if (!open) setSelectedFile(null);
|
if (!open) setSelectedFile(null);
|
||||||
}}
|
}}
|
||||||
file={selectedFile}
|
file={selectedFile}
|
||||||
|
user={id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableEditModal opened={tableEdit.open} onCLose={() => tableEdit.setOpen(false)} />
|
<TableEditModal opened={tableEdit.open} onCLose={() => tableEdit.setOpen(false)} />
|
||||||
@@ -427,48 +430,56 @@ export default function FileTable({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
color='yellow'
|
color='yellow'
|
||||||
leftSection={<IconStar size='1rem' />}
|
leftSection={<IconStar size='1rem' />}
|
||||||
onClick={() => bulkFavorite(selectedFiles.map((x) => x.id))}
|
onClick={() =>
|
||||||
|
bulkFavorite(
|
||||||
|
selectedFiles.map((x) => x.id),
|
||||||
|
!unfavoriteAll,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Favorite {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''}
|
{unfavoriteAll ? 'Unfavorite' : 'Favorite'} {selectedFiles.length} file
|
||||||
|
{selectedFiles.length > 1 ? 's' : ''}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Combobox
|
{!id && (
|
||||||
store={combobox}
|
<Combobox
|
||||||
withinPortal={false}
|
store={combobox}
|
||||||
onOptionSubmit={(value) => handleAddFolder(value)}
|
withinPortal={false}
|
||||||
>
|
onOptionSubmit={(value) => handleAddFolder(value)}
|
||||||
<Combobox.Target>
|
>
|
||||||
<InputBase
|
<Combobox.Target>
|
||||||
rightSection={<Combobox.Chevron />}
|
<InputBase
|
||||||
value={folderSearch}
|
rightSection={<Combobox.Chevron />}
|
||||||
onChange={(event) => {
|
value={folderSearch}
|
||||||
combobox.openDropdown();
|
onChange={(event) => {
|
||||||
combobox.updateSelectedOptionIndex();
|
combobox.openDropdown();
|
||||||
setFolderSearch(event.currentTarget.value);
|
combobox.updateSelectedOptionIndex();
|
||||||
}}
|
setFolderSearch(event.currentTarget.value);
|
||||||
onClick={() => combobox.openDropdown()}
|
}}
|
||||||
onFocus={() => combobox.openDropdown()}
|
onClick={() => combobox.openDropdown()}
|
||||||
onBlur={() => {
|
onFocus={() => combobox.openDropdown()}
|
||||||
combobox.closeDropdown();
|
onBlur={() => {
|
||||||
setFolderSearch(folderSearch || '');
|
combobox.closeDropdown();
|
||||||
}}
|
setFolderSearch(folderSearch || '');
|
||||||
placeholder='Add to folder...'
|
}}
|
||||||
rightSectionPointerEvents='none'
|
placeholder='Add to folder...'
|
||||||
/>
|
rightSectionPointerEvents='none'
|
||||||
</Combobox.Target>
|
/>
|
||||||
|
</Combobox.Target>
|
||||||
|
|
||||||
<Combobox.Dropdown>
|
<Combobox.Dropdown>
|
||||||
<Combobox.Options>
|
<Combobox.Options>
|
||||||
{folders
|
{folders
|
||||||
?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim()))
|
?.filter((f) => f.name.toLowerCase().includes(folderSearch.toLowerCase().trim()))
|
||||||
.map((f) => (
|
.map((f) => (
|
||||||
<Combobox.Option value={f.id} key={f.id}>
|
<Combobox.Option value={f.id} key={f.id}>
|
||||||
{f.name}
|
{f.name}
|
||||||
</Combobox.Option>
|
</Combobox.Option>
|
||||||
))}
|
))}
|
||||||
</Combobox.Options>
|
</Combobox.Options>
|
||||||
</Combobox.Dropdown>
|
</Combobox.Dropdown>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { File, fileSelect } from '@/lib/db/models/file';
|
|||||||
import { log } from '@/lib/logger';
|
import { log } from '@/lib/logger';
|
||||||
import { userMiddleware } from '@/server/middleware/user';
|
import { userMiddleware } from '@/server/middleware/user';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
import { canInteract } from '@/lib/role';
|
||||||
|
|
||||||
export type ApiUserFilesIdResponse = File;
|
export type ApiUserFilesIdResponse = File;
|
||||||
|
|
||||||
@@ -33,12 +34,13 @@ export default fastifyPlugin(
|
|||||||
const file = await prisma.file.findFirst({
|
const file = await prisma.file.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ id: req.params.id }, { name: req.params.id }],
|
OR: [{ id: req.params.id }, { name: req.params.id }],
|
||||||
userId: req.user.id,
|
|
||||||
},
|
},
|
||||||
select: fileSelect,
|
select: { User: true, ...fileSelect },
|
||||||
});
|
});
|
||||||
if (!file) return res.notFound();
|
if (!file) return res.notFound();
|
||||||
|
|
||||||
|
if (!canInteract(req.user.role, file.User?.role ?? 'USER')) return res.notFound();
|
||||||
|
|
||||||
return res.send(file);
|
return res.send(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,12 +51,13 @@ export default fastifyPlugin(
|
|||||||
const file = await prisma.file.findFirst({
|
const file = await prisma.file.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ id: req.params.id }, { name: req.params.id }],
|
OR: [{ id: req.params.id }, { name: req.params.id }],
|
||||||
userId: req.user.id,
|
|
||||||
},
|
},
|
||||||
select: fileSelect,
|
select: { User: true, ...fileSelect },
|
||||||
});
|
});
|
||||||
if (!file) return res.notFound();
|
if (!file) return res.notFound();
|
||||||
|
|
||||||
|
if (!canInteract(req.user.role, file.User?.role ?? 'USER')) return res.notFound();
|
||||||
|
|
||||||
const data: Prisma.FileUpdateInput = {};
|
const data: Prisma.FileUpdateInput = {};
|
||||||
|
|
||||||
if (req.body.favorite !== undefined) data.favorite = req.body.favorite;
|
if (req.body.favorite !== undefined) data.favorite = req.body.favorite;
|
||||||
@@ -126,6 +129,7 @@ export default fastifyPlugin(
|
|||||||
logger.info(`${req.user.username} updated file ${newFile.name}`, {
|
logger.info(`${req.user.username} updated file ${newFile.name}`, {
|
||||||
updated: Object.keys(req.body),
|
updated: Object.keys(req.body),
|
||||||
id: newFile.id,
|
id: newFile.id,
|
||||||
|
owner: file.User?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send(newFile);
|
return res.send(newFile);
|
||||||
@@ -135,11 +139,15 @@ export default fastifyPlugin(
|
|||||||
const file = await prisma.file.findFirst({
|
const file = await prisma.file.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ id: req.params.id }, { name: req.params.id }],
|
OR: [{ id: req.params.id }, { name: req.params.id }],
|
||||||
userId: req.user.id,
|
},
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!file) return res.notFound();
|
if (!file) return res.notFound();
|
||||||
|
|
||||||
|
if (!canInteract(req.user.role, file.User?.role ?? 'USER')) return res.notFound();
|
||||||
|
|
||||||
const deletedFile = await prisma.file.delete({
|
const deletedFile = await prisma.file.delete({
|
||||||
where: {
|
where: {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
@@ -151,6 +159,7 @@ export default fastifyPlugin(
|
|||||||
|
|
||||||
logger.info(`${req.user.username} deleted file ${deletedFile.name}`, {
|
logger.info(`${req.user.username} deleted file ${deletedFile.name}`, {
|
||||||
size: bytes(deletedFile.size),
|
size: bytes(deletedFile.size),
|
||||||
|
owner: file.User?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send(deletedFile);
|
return res.send(deletedFile);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { datasource } from '@/lib/datasource';
|
|||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { log } from '@/lib/logger';
|
import { log } from '@/lib/logger';
|
||||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||||
|
import { canInteract } from '@/lib/role';
|
||||||
|
import { Role } from '@/prisma/client';
|
||||||
import { userMiddleware } from '@/server/middleware/user';
|
import { userMiddleware } from '@/server/middleware/user';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
@@ -22,6 +24,18 @@ type Body = {
|
|||||||
|
|
||||||
const logger = log('api').c('user').c('files').c('transaction');
|
const logger = log('api').c('user').c('files').c('transaction');
|
||||||
|
|
||||||
|
function checkInteraction(current: Role, roles: Role[]) {
|
||||||
|
const indices: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i !== roles.length; ++i) {
|
||||||
|
if (!canInteract(current, roles[i])) {
|
||||||
|
indices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
export const PATH = '/api/user/files/transaction';
|
export const PATH = '/api/user/files/transaction';
|
||||||
export default fastifyPlugin(
|
export default fastifyPlugin(
|
||||||
(server, _, done) => {
|
(server, _, done) => {
|
||||||
@@ -34,14 +48,28 @@ export default fastifyPlugin(
|
|||||||
if (!files || !files.length) return res.badRequest('Cannot process transaction without files');
|
if (!files || !files.length) return res.badRequest('Cannot process transaction without files');
|
||||||
|
|
||||||
if (typeof favorite === 'boolean') {
|
if (typeof favorite === 'boolean') {
|
||||||
|
const toFavoriteFiles = await prisma.file.findMany({
|
||||||
|
where: {
|
||||||
|
id: { in: files },
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalids = checkInteraction(
|
||||||
|
req.user.role,
|
||||||
|
toFavoriteFiles.map((f) => f.User?.role ?? 'USER'),
|
||||||
|
);
|
||||||
|
if (invalids.length > 0)
|
||||||
|
return res.forbidden(`You don't have the permission to modify files[${invalids.join(', ')}]`);
|
||||||
|
|
||||||
const resp = await prisma.file.updateMany({
|
const resp = await prisma.file.updateMany({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
in: files,
|
in: files,
|
||||||
},
|
},
|
||||||
userId: req.user.id,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
favorite: favorite,
|
favorite: favorite,
|
||||||
},
|
},
|
||||||
@@ -51,6 +79,7 @@ export default fastifyPlugin(
|
|||||||
|
|
||||||
logger.info(`${req.user.username} ${favorite ? 'favorited' : 'unfavorited'} ${resp.count} files`, {
|
logger.info(`${req.user.username} ${favorite ? 'favorited' : 'unfavorited'} ${resp.count} files`, {
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
|
owners: toFavoriteFiles.map((f) => f.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send(resp);
|
return res.send(resp);
|
||||||
@@ -108,21 +137,28 @@ export default fastifyPlugin(
|
|||||||
files: files.length,
|
files: files.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (delete_datasourceFiles) {
|
const toDeleteFiles = await prisma.file.findMany({
|
||||||
const dFiles = await prisma.file.findMany({
|
where: {
|
||||||
where: {
|
id: { in: files },
|
||||||
id: {
|
},
|
||||||
in: files,
|
include: {
|
||||||
},
|
User: true,
|
||||||
userId: req.user.id,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i !== dFiles.length; ++i) {
|
const invalids = checkInteraction(
|
||||||
await datasource.delete(dFiles[i].name);
|
req.user.role,
|
||||||
|
toDeleteFiles.map((f) => f.User?.role ?? 'USER'),
|
||||||
|
);
|
||||||
|
if (invalids.length > 0)
|
||||||
|
return res.forbidden(`You don't have the permission to delete files[${invalids.join(', ')}]`);
|
||||||
|
|
||||||
|
if (delete_datasourceFiles) {
|
||||||
|
for (let i = 0; i !== toDeleteFiles.length; ++i) {
|
||||||
|
await datasource.delete(toDeleteFiles[i].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`${req.user.username} deleted ${dFiles.length} files from datasource`, {
|
logger.info(`${req.user.username} deleted ${toDeleteFiles.length} files from datasource`, {
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -132,7 +168,6 @@ export default fastifyPlugin(
|
|||||||
id: {
|
id: {
|
||||||
in: files,
|
in: files,
|
||||||
},
|
},
|
||||||
userId: req.user.id,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,6 +175,7 @@ export default fastifyPlugin(
|
|||||||
|
|
||||||
logger.info(`${req.user.username} deleted ${resp.count} files`, {
|
logger.info(`${req.user.username} deleted ${resp.count} files`, {
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
|
owners: toDeleteFiles.map((f) => f.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send(resp);
|
return res.send(resp);
|
||||||
|
|||||||
Reference in New Issue
Block a user