diff --git a/SECURITY.md b/SECURITY.md index 358a5c2..9f5e525 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | ------- | ------------------ | -| 3.4.4 | :white_check_mark: | +| 3.4.8 | :white_check_mark: | | < 3 | :x: | | < 2 | :x: | diff --git a/package.json b/package.json index 06bc351..7c81942 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "fflate": "^0.7.3", "find-my-way": "^6.3.0", "minio": "^7.0.28", + "ms": "canary", "multer": "^1.4.5-lts.1", "next": "^12.1.6", "prisma": "^4.1.0", diff --git a/src/components/File.tsx b/src/components/File.tsx index f49dcda..f0e36ed 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Group, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core'; +import { Button, Card, Group, Modal, Stack, Text, Title, Tooltip, useMantineTheme } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; import { showNotification } from '@mantine/notifications'; import useFetch from 'hooks/useFetch'; @@ -8,8 +8,18 @@ import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, Has import MutedText from './MutedText'; import { relativeTime } from 'lib/clientUtils'; -export function FileMeta({ Icon, title, subtitle }) { - return ( +export function FileMeta({ Icon, title, subtitle, ...other }) { + return other.tooltip ? ( + + + + + {title} + {subtitle} + + + + ) : ( @@ -88,7 +98,12 @@ export default function File({ image, updateImages }) { - {image.expires_at && } + {image.expires_at && } diff --git a/src/components/pages/Upload.tsx b/src/components/pages/Upload.tsx index 49198c2..6417a3a 100644 --- a/src/components/pages/Upload.tsx +++ b/src/components/pages/Upload.tsx @@ -140,7 +140,7 @@ export default function Upload() { req.open('POST', '/api/upload'); req.setRequestHeader('Authorization', user.token); - expires !== 'never' && req.setRequestHeader('Expires-At', expires_at.toISOString()); + expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString()); req.send(body); }; diff --git a/src/lib/clientUtils.ts b/src/lib/clientUtils.ts index e1b09dc..17851a2 100644 --- a/src/lib/clientUtils.ts +++ b/src/lib/clientUtils.ts @@ -1,4 +1,5 @@ -import type { Image, User } from '@prisma/client'; +import type { Image, ImageFormat, User } from '@prisma/client'; +import ms, { StringValue } from 'ms'; export function parse(str: string, image: Image, user: User) { if (!str) return null; @@ -50,3 +51,35 @@ export function relativeTime(to: Date, from: Date = new Date()) { } } +export function humanTime(string: StringValue | string): Date { + try { + const mil = ms(string as StringValue); + if (typeof mil !== 'number') return null; + if (isNaN(mil)) return null; + if (!mil) return null; + + return new Date(Date.now() + mil); + } catch (_) { + return null; + } +} + +export function parseExpiry(header: string): Date | null { + if (!header) return null; + header = header.toLowerCase(); + + if (header.startsWith('date=')) { + const date = new Date(header.substring(5)); + + if (!date.getTime()) return null; + if (date.getTime() < Date.now()) return null; + return date; + } + + const human = humanTime(header); + + if (!human) return null; + if (human.getTime() < Date.now()) return null; + + return human; +} \ No newline at end of file diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index f7ea556..ad68298 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -9,6 +9,8 @@ import { format as formatDate } from 'fecha'; import datasource from 'lib/datasource'; import { randomUUID } from 'crypto'; import sharp from 'sharp'; +import { humanTime, parseExpiry } from 'lib/clientUtils'; +import { StringValue } from 'ms'; const uploader = multer(); @@ -42,21 +44,27 @@ async function handler(req: NextApiReq, res: NextApiRes) { await run(uploader.array('file'))(req, res); if (!req.files) return res.error('no files'); - if (req.files && req.files.length === 0) return res.error('no files'); + const response: { files: string[], expires_at?: Date } = { files: [] }; + const expires_at = req.headers['expires-at'] as string; - const expiry = expires_at ? new Date(expires_at) : null; - if (expiry) { - if (!expiry.getTime()) return res.bad('invalid date'); - if (expiry.getTime() < Date.now()) return res.bad('date is in the past'); + let expiry: Date; + + if (expires_at) { + expiry = parseExpiry(expires_at); + if (!expiry) return res.error('invalid date'); + else { + response.expires_at = expiry; + } } const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM'; const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat]; + const imageCompressionPercent = req.headers['image-compression-percent'] ? Number(req.headers['image-compression-percent']) : null; - const files = []; + for (let i = 0; i !== req.files.length; ++i) { const file = req.files[i]; if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error(`file[${i}] size too big`); @@ -78,6 +86,9 @@ async function handler(req: NextApiReq, res: NextApiRes) { case ImageFormat.NAME: fileName = file.originalname.split('.')[0]; break; + default: + fileName = randomChars(zconfig.uploader.length); + break; } let password = null; @@ -90,7 +101,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { const image = await prisma.image.create({ data: { file: `${fileName}.${compressionUsed ? 'jpg' : ext}`, - mimetype: req.headers.uploadtext ? 'text/plain' : file.mimetype, + mimetype: req.headers.uploadtext ? 'text/plain' : (compressionUsed ? 'image/jpeg' : file.mimetype), userId: user.id, embed: !!req.headers.embed, format, @@ -112,9 +123,9 @@ async function handler(req: NextApiReq, res: NextApiRes) { Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`); if (user.domains.length) { const domain = user.domains[Math.floor(Math.random() * user.domains.length)]; - files.push(`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`); + response.files.push(`${domain}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`); } else { - files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`); + response.files.push(`${zconfig.core.https ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route === '/' ? '' : zconfig.uploader.route}/${invis ? invis.invis : image.file}`); } } @@ -140,7 +151,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } - return res.json({ files }); + return res.json(response); } function run(middleware: any) { diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts index cc42f00..fd0f7d8 100644 --- a/src/pages/api/user/recent.ts +++ b/src/pages/api/user/recent.ts @@ -19,6 +19,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, select: { created_at: true, + expires_at: true, file: true, mimetype: true, id: true, diff --git a/yarn.lock b/yarn.lock index 4062406..25ad837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6009,6 +6009,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:canary": + version: 3.0.0-canary.1 + resolution: "ms@npm:3.0.0-canary.1" + checksum: 5ec76c0932cf83ac3e7f70f1a4c0d4db4dbc91de6ea5f7d336c67b48f513c8cb4c0fce3a07e3d84ee931dbdc9a48f33ed1c485e834279fff8906d385e86684ae + languageName: node + linkType: hard + "mssql@npm:8.1.2": version: 8.1.2 resolution: "mssql@npm:8.1.2" @@ -8896,6 +8903,7 @@ __metadata: fflate: ^0.7.3 find-my-way: ^6.3.0 minio: ^7.0.28 + ms: canary multer: ^1.4.5-lts.1 next: ^12.1.6 npm-run-all: ^4.1.5