diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 43b1022..3d158e2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -23,8 +23,8 @@ services: env_file: - .env.local volumes: - - '$PWD/uploads:/zipline/uploads' - - '$PWD/public:/zipline/public' + - './uploads:/zipline/uploads' + - './public:/zipline/public' depends_on: - 'postgres' diff --git a/docker-compose.yml b/docker-compose.yml index d3cc2f3..d42369c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,7 +29,7 @@ services: - CORE_LOGGER=true volumes: - './uploads:/zipline/uploads' - - '$PWD/public:/zipline/public' + - './public:/zipline/public' depends_on: - 'postgres' diff --git a/src/components/pages/Dashboard/index.tsx b/src/components/pages/Dashboard/index.tsx index 4e0a6f1..8de2918 100644 --- a/src/components/pages/Dashboard/index.tsx +++ b/src/components/pages/Dashboard/index.tsx @@ -12,7 +12,7 @@ import { import FileModal from 'components/File/FileModal'; import MutedText from 'components/MutedText'; import useFetch from 'lib/hooks/useFetch'; -import { usePaginatedFiles, useRecent } from 'lib/queries/files'; +import { PaginatedFilesOptions, usePaginatedFiles, useRecent } from 'lib/queries/files'; import { useStats } from 'lib/queries/stats'; import { userSelector } from 'lib/recoil/user'; import { bytesToHuman } from 'lib/utils/bytes'; @@ -45,32 +45,24 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress } })(); }, [page]); - const files = usePaginatedFiles(page, 'none'); - // sorting const [sortStatus, setSortStatus] = useState({ - columnAccessor: 'date', + columnAccessor: 'createdAt', direction: 'asc', }); - const [records, setRecords] = useState(files.data); - useEffect(() => { - setRecords(files.data); - }, [files.data]); + const files = usePaginatedFiles(page, { + filter: 'none', - useEffect(() => { - if (!records || records.length === 0) return; - - const sortedRecords = [...records].sort((a, b) => { - if (sortStatus.direction === 'asc') { - return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1; - } - - return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1; - }); - - setRecords(sortedRecords); - }, [sortStatus]); + // only query for correct results if there is more than one page + // otherwise, querying has no effect + ...(numFiles > 1 + ? { + sortBy: sortStatus.columnAccessor as PaginatedFilesOptions['sortBy'], + order: sortStatus.direction, + } + : {}), + }); // file modal on click const [open, setOpen] = useState(false); @@ -203,7 +195,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress } ), }, ]} - records={records ?? []} + records={files.data ?? []} fetching={files.isLoading} loaderBackgroundBlur={5} loaderVariant='dots' diff --git a/src/components/pages/Files/FilePagation.tsx b/src/components/pages/Files/FilePagation.tsx index 57aba25..7303980 100644 --- a/src/components/pages/Files/FilePagation.tsx +++ b/src/components/pages/Files/FilePagation.tsx @@ -37,7 +37,9 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa })(); }, [page]); - const pages = usePaginatedFiles(page, !checked ? 'media' : null); + const pages = usePaginatedFiles(page, { + filter: !checked ? 'media' : 'none', + }); if (pages.isSuccess && pages.data.length === 0) { if (page > 1 && numPages > 0) { diff --git a/src/components/pages/Files/index.tsx b/src/components/pages/Files/index.tsx index 9a03bbe..abde034 100644 --- a/src/components/pages/Files/index.tsx +++ b/src/components/pages/Files/index.tsx @@ -11,7 +11,10 @@ import PendingFilesModal from './PendingFilesModal'; export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) { const [favoritePage, setFavoritePage] = useState(1); const [favoriteNumPages, setFavoriteNumPages] = useState(0); - const favoritePages = usePaginatedFiles(favoritePage, 'media', true); + const favoritePages = usePaginatedFiles(favoritePage, { + filter: 'media', + favorite: true, + }); const [open, setOpen] = useState(false); diff --git a/src/lib/discord.ts b/src/lib/discord.ts index 745d018..a73db8f 100644 --- a/src/lib/discord.ts +++ b/src/lib/discord.ts @@ -63,13 +63,13 @@ export async function sendUpload(user: User, file: File, raw_link: string, link: thumbnail: isImage && parsed.embed.thumbnail ? { - url: parsed.url, + url: raw_link, } : null, image: isImage && parsed.embed.image ? { - url: parsed.url, + url: raw_link, } : null, }, diff --git a/src/lib/middleware/getServerSideProps.ts b/src/lib/middleware/getServerSideProps.ts index 8e09094..5872434 100644 --- a/src/lib/middleware/getServerSideProps.ts +++ b/src/lib/middleware/getServerSideProps.ts @@ -75,7 +75,7 @@ export const getServerSideProps: GetServerSideProps = async (ct user_registration: config.features.user_registration, oauth_registration: config.features.oauth_registration, oauth_providers: JSON.stringify(oauth_providers), - bypass_local_login: config.oauth.bypass_local_login, + bypass_local_login: config.oauth?.bypass_local_login ?? false, chunks_size: config.chunks.chunks_size, max_size: config.chunks.max_size, totp_enabled: config.mfa.totp_enabled, diff --git a/src/lib/queries/files.ts b/src/lib/queries/files.ts index 47cbfdb..6740c91 100644 --- a/src/lib/queries/files.ts +++ b/src/lib/queries/files.ts @@ -33,13 +33,23 @@ export const useFiles = (query: { [key: string]: string } = {}) => { ); }); }; -export const usePaginatedFiles = (page?: number, filter = 'media', favorite = null) => { - const queryBuilder = new URLSearchParams({ + +export type PaginatedFilesOptions = { + filter: 'media' | 'none'; + favorite: boolean; + sortBy: 'createdAt' | 'views' | 'expiresAt' | 'size' | 'name' | 'mimetype'; + order: 'asc' | 'desc'; +}; + +export const usePaginatedFiles = (page?: number, options?: Partial) => { + const queryString = new URLSearchParams({ page: Number(page || '1').toString(), - filter, - ...(favorite !== null && { favorite: favorite.toString() }), - }); - const queryString = queryBuilder.toString(); + filter: options?.filter ?? 'none', + // ...(options?.favorite !== null && { favorite: options?.favorite?.toString() }), + favorite: options.favorite ? 'true' : '', + sortBy: options.sortBy ?? '', + order: options.order ?? '', + }).toString(); return useQuery(['files', queryString], async () => { return fetch('/api/user/paged?' + queryString) diff --git a/src/pages/500.tsx b/src/pages/500.tsx index ad84d61..e6f3e15 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -2,8 +2,10 @@ import { Button, Stack, Title, Tooltip } from '@mantine/core'; import MutedText from 'components/MutedText'; import Head from 'next/head'; import Link from 'next/link'; +import { useRouter } from 'next/router'; export default function FiveHundred() { + const { asPath } = useRouter(); return ( <> @@ -24,9 +26,13 @@ export default function FiveHundred() { Internal server error - + {asPath === '/dashboard' ? ( + + ) : ( + + )} ); diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 5e3dc9e..522d297 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -255,7 +255,12 @@ async function handler(req: NextApiReq, res: NextApiRes) { response.files.push(responseUrl); if (zconfig.discord?.upload) { - await sendUpload(user, fileUpload, `${domain}/r/${invis ? invis.invis : fileUpload.name}`, responseUrl); + await sendUpload( + user, + fileUpload, + `${domain}/r/${invis ? invis.invis : encodeURI(fileUpload.name)}`, + responseUrl + ); } if (zconfig.exif.enabled && zconfig.exif.remove_gps && fileUpload.mimetype.startsWith('image/')) { diff --git a/src/pages/api/user/paged.ts b/src/pages/api/user/paged.ts index 0210257..534c6cb 100644 --- a/src/pages/api/user/paged.ts +++ b/src/pages/api/user/paged.ts @@ -1,3 +1,5 @@ +import { Prisma } from '@prisma/client'; +import { s } from '@sapphire/shapeshift'; import config from 'lib/config'; import prisma from 'lib/prisma'; import { formatRootUrl } from 'lib/utils/urls'; @@ -5,12 +7,27 @@ import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/wi const pageCount = 16; +const sortByValidator = s.enum( + ...([ + 'createdAt', + 'views', + 'expiresAt', + 'size', + 'name', + 'mimetype', + ] satisfies (keyof Prisma.FileOrderByWithRelationInput)[]) +); + +const orderValidator = s.enum('asc', 'desc'); + async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { - const { page, filter, count, favorite } = req.query as { + const { page, filter, count, favorite, ...rest } = req.query as { page: string; filter: string; count: string; favorite: string; + sortBy: string; + order: string; }; const where = { @@ -33,7 +50,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { }, ], }), - }; + } satisfies Prisma.FileWhereInput; if (count) { const count = await prisma.file.count({ @@ -48,6 +65,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (!page) return res.badRequest('no page'); if (isNaN(Number(page))) return res.badRequest('page is not a number'); + // validate sortBy + const sortBy = sortByValidator.run(rest.sortBy || 'createdAt'); + if (!sortBy.isOk()) return res.badRequest('invalid sortBy option'); + + // validate order + const order = orderValidator.run(rest.order || 'desc'); + if (!sortBy.isOk()) return res.badRequest('invalid order option'); + const files: { favorite: boolean; createdAt: Date; @@ -63,7 +88,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { }[] = await prisma.file.findMany({ where, orderBy: { - createdAt: 'desc', + [sortBy.value]: order.value, }, select: { createdAt: true,