mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
fix: random visual bugs + enhancements
This commit is contained in:
10
SECURITY.md
10
SECURITY.md
@@ -2,11 +2,11 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------------------------- |
|
||||
| 4.x.x | :white_check_mark: |
|
||||
| < 3 | :white_check_mark: (EOL at June 2025) |
|
||||
| < 2 | :x: |
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.2.x | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 2 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Modal,
|
||||
Pill,
|
||||
PillsInput,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
Title,
|
||||
@@ -61,8 +60,8 @@ import {
|
||||
removeFromFolder,
|
||||
viewFile,
|
||||
} from '../actions';
|
||||
import FileStat from './FileStat';
|
||||
import EditFileDetailsModal from './EditFileDetailsModal';
|
||||
import FileStat from './FileStat';
|
||||
|
||||
function ActionButton({
|
||||
Icon,
|
||||
@@ -189,9 +188,9 @@ export default function FileModal({
|
||||
</Text>
|
||||
}
|
||||
size='auto'
|
||||
maw='90vw'
|
||||
centered
|
||||
zIndex={200}
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
>
|
||||
{file ? (
|
||||
<>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { Icon, IconFileUnknown, IconPlayerPlay, IconShieldLockFilled } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { renderMode } from '../pages/upload/renderMode';
|
||||
import Render from '../render/Render';
|
||||
import fileIcon from './fileIcon';
|
||||
@@ -30,7 +30,7 @@ function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
|
||||
|
||||
function Placeholder({ text, Icon, ...props }: { text: string; Icon: Icon; onClick?: () => void }) {
|
||||
return (
|
||||
<Center py='xs' style={{ height: '100%', width: '100%', cursor: 'pointed' }} {...props}>
|
||||
<Center py='xs' style={{ height: '100%', width: '100%', cursor: 'pointer' }} {...props}>
|
||||
<PlaceholderContent text={text} Icon={Icon} />
|
||||
</Center>
|
||||
);
|
||||
@@ -83,57 +83,60 @@ export default function DashboardFileType({
|
||||
const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview);
|
||||
|
||||
const dbFile = 'id' in file;
|
||||
const renderIn = renderMode(file.name.split('.').pop() || '');
|
||||
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
|
||||
|
||||
const [fileContent, setFileContent] = useState('');
|
||||
const [type, setType] = useState<string>(file.type.split('/')[0]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const gettext = async () => {
|
||||
if (!dbFile) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if ((reader.result! as string).length > 1 * 1024 * 1024) {
|
||||
setFileContent(
|
||||
reader.result!.slice(0, 1 * 1024 * 1024) +
|
||||
'\n...\nThe file is too big to display click the download icon to view/download it.',
|
||||
);
|
||||
} else {
|
||||
setFileContent(reader.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
const getText = useCallback(async () => {
|
||||
try {
|
||||
if (!dbFile) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if ((reader.result! as string).length > 1 * 1024 * 1024) {
|
||||
setFileContent(
|
||||
reader.result!.slice(0, 1 * 1024 * 1024) +
|
||||
'\n...\nThe file is too big to display click the download icon to view/download it.',
|
||||
);
|
||||
} else {
|
||||
setFileContent(reader.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 1 * 1024 * 1024) {
|
||||
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`, {
|
||||
headers: {
|
||||
Range: 'bytes=0-' + 1 * 1024 * 1024, // 0 mb to 1 mb
|
||||
},
|
||||
});
|
||||
if (file.size > 1 * 1024 * 1024) {
|
||||
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`, {
|
||||
headers: {
|
||||
Range: 'bytes=0-' + 1 * 1024 * 1024, // 0 mb to 1 mb
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch file');
|
||||
const text = await res.text();
|
||||
setFileContent(
|
||||
text + '\n...\nThe file is too big to display click the download icon to view/download it.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`);
|
||||
if (!res.ok) throw new Error('Failed to fetch file');
|
||||
const text = await res.text();
|
||||
setFileContent(
|
||||
text + '\n...\nThe file is too big to display click the download icon to view/download it.',
|
||||
);
|
||||
return;
|
||||
setFileContent(text);
|
||||
} catch {
|
||||
setFileContent('Error loading file.');
|
||||
}
|
||||
|
||||
const res = await fetch(`/raw/${file.name}${password ? `?pw=${password}` : ''}`);
|
||||
const text = await res.text();
|
||||
|
||||
setFileContent(text);
|
||||
};
|
||||
}, [dbFile, file, password]);
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
setType('text');
|
||||
gettext();
|
||||
getText();
|
||||
} else if (overrideType === 'text' || type === 'text') {
|
||||
gettext();
|
||||
getText();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -177,7 +180,10 @@ export default function DashboardFileType({
|
||||
/>
|
||||
) : (file as DbFile).thumbnail && dbFile ? (
|
||||
<Box pos='relative'>
|
||||
<MantineImage src={`/raw/${(file as DbFile).thumbnail!.path}`} alt={file.name} />
|
||||
<MantineImage
|
||||
src={`/raw/${(file as DbFile).thumbnail!.path}`}
|
||||
alt={file.name || 'Video thumbnail'}
|
||||
/>
|
||||
|
||||
<Center
|
||||
pos='absolute'
|
||||
@@ -203,7 +209,7 @@ export default function DashboardFileType({
|
||||
<Center>
|
||||
<MantineImage
|
||||
src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
alt={file.name || 'Image'}
|
||||
style={{
|
||||
cursor: allowZoom ? 'zoom-in' : 'default',
|
||||
maxWidth: '70vw',
|
||||
@@ -217,7 +223,7 @@ export default function DashboardFileType({
|
||||
src={
|
||||
dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)
|
||||
}
|
||||
alt={file.name}
|
||||
alt={file.name || 'Image'}
|
||||
style={{
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '95vh',
|
||||
@@ -234,7 +240,7 @@ export default function DashboardFileType({
|
||||
fit='contain'
|
||||
mah={400}
|
||||
src={dbFile ? `/raw/${file.name}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
alt={file.name || 'Image'}
|
||||
/>
|
||||
);
|
||||
case 'audio':
|
||||
|
||||
@@ -7,6 +7,7 @@ import FilesUrlsCountGraph from './parts/FilesUrlsCountGraph';
|
||||
import { useApiStats } from './useStats';
|
||||
import { StatsCardsSkeleton } from './parts/StatsCards';
|
||||
import { StatsTablesSkeleton } from './parts/StatsTables';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const StatsCards = dynamic(() => import('./parts/StatsCards'));
|
||||
const StatsTables = dynamic(() => import('./parts/StatsTables'));
|
||||
@@ -14,9 +15,11 @@ const StorageGraph = dynamic(() => import('./parts/StorageGraph'));
|
||||
const ViewsGraph = dynamic(() => import('./parts/ViewsGraph'));
|
||||
|
||||
export default function DashboardMetrics() {
|
||||
const today = dayjs();
|
||||
|
||||
const [dateRange, setDateRange] = useState<[string | null, string | null]>([
|
||||
new Date(Date.now() - 86400000 * 7).toISOString(),
|
||||
new Date().toISOString(),
|
||||
today.subtract(7, 'day').toISOString(),
|
||||
today.toISOString(),
|
||||
]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -40,17 +43,49 @@ export default function DashboardMetrics() {
|
||||
return (
|
||||
<>
|
||||
<Modal title='Change range' opened={open} onClose={() => setOpen(false)} size='auto'>
|
||||
<Paper withBorder>
|
||||
<Paper withBorder style={{ minHeight: 300 }}>
|
||||
<DatePicker
|
||||
type='range'
|
||||
value={dateRange}
|
||||
onChange={handleDateChange}
|
||||
allowSingleDateInRange={false}
|
||||
maxDate={new Date()}
|
||||
presets={[
|
||||
{
|
||||
value: [today.subtract(2, 'day').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
|
||||
label: 'Last two days',
|
||||
},
|
||||
{
|
||||
value: [today.subtract(7, 'day').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
|
||||
label: 'Last 7 days',
|
||||
},
|
||||
{
|
||||
value: [today.startOf('month').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
|
||||
label: 'This month',
|
||||
},
|
||||
{
|
||||
value: [
|
||||
today.subtract(1, 'month').startOf('month').format('YYYY-MM-DD'),
|
||||
today.subtract(1, 'month').endOf('month').format('YYYY-MM-DD'),
|
||||
],
|
||||
label: 'Last month',
|
||||
},
|
||||
{
|
||||
value: [today.startOf('year').format('YYYY-MM-DD'), today.format('YYYY-MM-DD')],
|
||||
label: 'This year',
|
||||
},
|
||||
{
|
||||
value: [
|
||||
today.subtract(1, 'year').startOf('year').format('YYYY-MM-DD'),
|
||||
today.subtract(1, 'year').endOf('year').format('YYYY-MM-DD'),
|
||||
],
|
||||
label: 'Last year',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Group mt='md'>
|
||||
<Group mt='lg'>
|
||||
<Button fullWidth onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
@@ -63,12 +63,35 @@ export default function Domains({
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid mt='md' cols={{ base: 1, sm: 2, md: 3 }} spacing='xs'>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, sm: 2, md: 3 }} spacing='md' verticalSpacing='md'>
|
||||
{domains.map((domain, index) => (
|
||||
<Paper key={index} withBorder p='xs'>
|
||||
<Group justify='space-between'>
|
||||
<div>
|
||||
<strong>{domain}</strong>
|
||||
<Paper
|
||||
key={index}
|
||||
withBorder
|
||||
p='md'
|
||||
radius='md'
|
||||
shadow='xs'
|
||||
style={{
|
||||
background: 'rgba(0,0,0,0.03)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
minHeight: 64,
|
||||
}}
|
||||
>
|
||||
<Group justify='space-between' align='center' wrap='nowrap'>
|
||||
<div
|
||||
style={{
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontWeight: 500,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{domain}
|
||||
</div>
|
||||
<Button
|
||||
variant='subtle'
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useClipboard, useColorScheme } from '@mantine/hooks';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import { IconDeviceSdCard, IconFiles, IconUpload, IconX } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import UploadOptionsButton from '../UploadOptionsButton';
|
||||
import { uploadFiles } from '../uploadFiles';
|
||||
import ToUploadFile from './ToUploadFile';
|
||||
@@ -49,34 +49,23 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
});
|
||||
const [dropLoading, setLoading] = useState(false);
|
||||
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
if (!e.clipboardData) return;
|
||||
const aggSize = useCallback(() => files.reduce((acc, file) => acc + file.size, 0), [files]);
|
||||
|
||||
const handlePaste = useCallback((e: ClipboardEvent) => {
|
||||
if (!e.clipboardData) return;
|
||||
for (let i = 0; i !== e.clipboardData.items.length; ++i) {
|
||||
if (!e.clipboardData.items[i].type.startsWith('image')) return;
|
||||
|
||||
const blob = e.clipboardData.items[i].getAsFile();
|
||||
if (!blob) return;
|
||||
|
||||
setFiles([...files, blob]);
|
||||
showNotification({
|
||||
message: `Image ${blob.name} pasted from clipboard`,
|
||||
color: 'blue',
|
||||
});
|
||||
setFiles((prev) => [...prev, blob]);
|
||||
showNotification({ message: `Image ${blob.name} pasted from clipboard`, color: 'blue' });
|
||||
}
|
||||
};
|
||||
|
||||
const aggSize = () => files.reduce((acc, file) => acc + file.size, 0);
|
||||
}, []);
|
||||
|
||||
const upload = () => {
|
||||
const toPartialFiles: File[] = [];
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
if (config.chunks.enabled && file.size >= bytes(config.chunks.max)) {
|
||||
toPartialFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
const toPartialFiles: File[] = files.filter(
|
||||
(file) => config.chunks.enabled && file.size >= bytes(config.chunks.max),
|
||||
);
|
||||
if (toPartialFiles.length > 0) {
|
||||
uploadPartialFiles(toPartialFiles, {
|
||||
setFiles,
|
||||
@@ -91,7 +80,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
});
|
||||
} else {
|
||||
const size = aggSize();
|
||||
if (size > bytes(config.files.maxFileSize) && !toPartialFiles.length) {
|
||||
if (size > bytes(config.files.maxFileSize)) {
|
||||
notifications.show({
|
||||
title: 'Upload may fail',
|
||||
color: 'yellow',
|
||||
@@ -105,7 +94,6 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
uploadFiles(files, {
|
||||
setFiles,
|
||||
setLoading,
|
||||
@@ -121,11 +109,22 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('paste', handlePaste);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}, []);
|
||||
}, [handlePaste]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [files.length]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -142,7 +141,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
</Group>
|
||||
|
||||
<Dropzone
|
||||
onDrop={(f) => setFiles([...f, ...files])}
|
||||
onDrop={(f) => setFiles((prev) => [...f, ...prev])}
|
||||
my='sm'
|
||||
loading={dropLoading}
|
||||
disabled={dropLoading}
|
||||
@@ -220,7 +219,6 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
|
||||
|
||||
<Group justify='right' gap='sm' my='md'>
|
||||
<UploadOptionsButton folder={folder} numFiles={files.length} />
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
leftSection={<IconUpload size={18} />}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { IconCursorText, IconEyeFilled, IconFiles, IconUpload } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import UploadOptionsButton from '../UploadOptionsButton';
|
||||
import { renderMode } from '../renderMode';
|
||||
import { uploadFiles } from '../uploadFiles';
|
||||
@@ -30,15 +30,26 @@ export default function UploadText({
|
||||
codeMeta: Parameters<typeof DashboardUploadText>[0]['codeMeta'];
|
||||
}) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore(
|
||||
useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]),
|
||||
);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState('txt');
|
||||
const [text, setText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (text.length > 0) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [text]);
|
||||
|
||||
const renderIn = renderMode(selectedLanguage);
|
||||
|
||||
const handleTab = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -52,12 +63,10 @@ export default function UploadText({
|
||||
|
||||
const upload = () => {
|
||||
const blob = new Blob([text]);
|
||||
|
||||
const file = new File([blob], `text.${selectedLanguage}`, {
|
||||
type: codeMeta.find((meta) => meta.ext === selectedLanguage)?.mime,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
uploadFiles([file], {
|
||||
clipboard,
|
||||
setFiles: () => {},
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function HighlightCode({ language, code }: { language: string; co
|
||||
</CopyButton>
|
||||
|
||||
<ScrollArea type='auto' dir='ltr' offsetScrollbars={false}>
|
||||
<pre style={{ margin: 0 }} className='theme'>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre', overflowX: 'auto' }} className='theme'>
|
||||
<code className='theme'>
|
||||
{displayLines.map((line, i) => (
|
||||
<div key={i}>
|
||||
|
||||
@@ -143,6 +143,10 @@ export async function read() {
|
||||
const database = (await readDatabaseSettings()) as Record<string, any>;
|
||||
const { dbEnv, env } = readEnv();
|
||||
|
||||
if (global.__tamperedConfig__) {
|
||||
global.__tamperedConfig__ = [];
|
||||
}
|
||||
|
||||
// this overwrites database settings with provided env vars if they exist
|
||||
for (const [propPath, val] of Object.entries(dbEnv)) {
|
||||
const col = Object.entries(DATABASE_TO_PROP).find(([_colName, path]) => path === propPath)?.[0];
|
||||
|
||||
Reference in New Issue
Block a user