feat: a bunch of changes

This commit is contained in:
dicedtomato 2022-07-06 16:57:39 +00:00 committed by GitHub
parent 20c1d3ef08
commit a999abfbf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1312 additions and 1638 deletions

View file

@ -17,7 +17,7 @@
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-typos": "error",
"react/react-in-jsx-scope": "error",
"react/react-in-jsx-scope": "off",
"react/require-render-return": "error",
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off"

View file

@ -57,7 +57,7 @@ yarn start
```
# NGINX Proxy
This section requires [nginx](https://nginx.org/).
This section requires [NGINX](https://nginx.org/).
```nginx
server {
@ -97,7 +97,7 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
# Contributing
## Bug reports
Create an issue on GitHub, please include the following:
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
* The steps to reproduce the bug
* Logs of Zipline
* The version of Zipline

View file

@ -1,10 +1,32 @@
[core]
secure = true
secret = 'changethis'
secure = true # whether to return https or http in links
secret = 'changethis' # change this or zipline will not work
host = '0.0.0.0'
port = 3000
database_url = 'postgres://postgres:postgres@postgres/postgres'
[datasource]
type = 'local' # s3, local, or swift
[datasource.local]
directory = './uploads' # directory to store uploads in
[datasource.s3]
access_key_id = 'AKIAEXAMPLEKEY'
secret_access_key = 'somethingsomethingsomething'
bucket = 'zipline-storage'
endpoint = 's3.amazonaws.com'
region = 'us-west-2' # not required, defaults to us-east-1 if not specified
force_s3_path = false
[datasource.swift]
container = 'default'
auth_endpoint = 'http://127.0.0.1:49155/v3' # only supports v3 swift endpoints at the moment.
username = 'swift'
password = 'fingertips'
project_id = 'Default'
domain_id = 'Default'
[urls]
route = '/go'
length = 6
@ -13,7 +35,10 @@ length = 6
route = '/u'
embed_route = '/a'
length = 6
directory = './uploads'
user_limit = 104900000 # 100mb
admin_limit = 104900000 # 100mb
disabled_extensions = ['jpg']
disabled_extensions = ['jpg']
[ratelimit]
user = 5 # 5 seconds
admin = 0 # 0 seconds, disabled

View file

@ -20,6 +20,8 @@ const { rm } = require('fs/promises');
'src/server/util.ts',
'src/lib/logger.ts',
'src/lib/config.ts',
'src/lib/mimes.ts',
'src/lib/exts.ts',
'src/lib/config/Config.ts',
'src/lib/config/readConfig.ts',
'src/lib/config/validateConfig.ts',

View file

@ -11,4 +11,6 @@ module.exports = {
api: {
responseLimit: false,
},
poweredByHeader: false,
reactStrictMode: true,
};

View file

@ -44,7 +44,6 @@
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"redux": "^4.2.0",
"uuid": "^8.3.2",
"yup": "^0.32.11"
},
"devDependencies": {

View file

@ -1,35 +0,0 @@
const { readdir } = require('fs/promises');
const { extname } = require('path');
const validateConfig = require('../server/validateConfig');
const Logger = require('../src/lib/logger');
const readConfig = require('../src/lib/readConfig');
const mimes = require('./mimes');
const { PrismaClient } = require('@prisma/client');
(async () => {
const config = readConfig();
await validateConfig(config);
process.env.DATABASE_URL = config.core.database_url;
const files = await readdir(process.argv[2]);
const data = files.map(x => {
const mime = mimes[extname(x)] ?? 'application/octet-stream';
return {
file: x,
mimetype: mime,
userId: 1,
};
});
const prisma = new PrismaClient();
Logger.get('migrator').info('starting migrations...');
await prisma.image.createMany({
data,
});
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
process.exit();
})();

View file

@ -1,8 +0,0 @@
import React from 'react';
import { LoadingOverlay } from '@mantine/core';
export default function Backdrop({ open }) {
return (
<LoadingOverlay visible={open} />
);
}

View file

@ -1,14 +1,9 @@
import React from 'react';
import {
Card as MCard,
Title,
} from '@mantine/core';
import { Card as MCard, Title } from '@mantine/core';
export default function Card(props) {
const { name, children, ...other } = props;
export default function Card({ name, children, ...other }) {
return (
<MCard padding='md' shadow='sm' {...other}>
<MCard p='md' shadow='sm' {...other}>
<Title order={2}>{name}</Title>
{children}
</MCard>

View file

@ -1,11 +1,11 @@
import React, { useState } from 'react';
import useFetch from 'hooks/useFetch';
import { Button, Card, Group, Image as MImage, Modal, Title } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { CopyIcon, Cross1Icon, StarIcon, TrashIcon } from '@modulz/radix-icons';
import { useClipboard } from '@mantine/hooks';
import useFetch from 'hooks/useFetch';
import { useState } from 'react';
export default function Image({ image, updateImages }) {
export default function File({ image, updateImages }) {
const [open, setOpen] = useState(false);
const [t] = useState(image.mimetype.split('/')[0]);
const notif = useNotifications();
@ -56,7 +56,7 @@ export default function Image({ image, updateImages }) {
const Type = (props) => {
return {
'video': <video controls {...props} />,
'image': <MImage {...props} />,
'image': <MImage withPlaceholder {...props} />,
'audio': <audio controls {...props} />,
}[t];
};

View file

@ -1,17 +1,12 @@
/* eslint-disable jsx-a11y/alt-text */
/* eslint-disable react/jsx-key */
/* eslint-disable react/display-name */
// Code taken from https://codesandbox.io/s/eojw8 and is modified a bit (the entire src/components/table directory)
import React from 'react';
import {
usePagination,
useTable,
} from 'react-table';
import {
ActionIcon,
createStyles,
Divider,
Group,
Pagination,
Group, Image, Pagination,
Select,
Table,
Text,
@ -22,6 +17,10 @@ import {
EnterIcon,
TrashIcon,
} from '@modulz/radix-icons';
import {
usePagination,
useTable,
} from 'react-table';
const pageSizeOptions = ['10', '25', '50'];
@ -42,6 +41,26 @@ const useStyles = createStyles((t) => ({
sortDirectionIcon: { transition: 'transform 200ms ease' },
}));
export function FilePreview({ url, type }) {
const Type = props => {
return {
'video': <video autoPlay controls {...props} />,
'image': <Image {...props} />,
'audio': <audio autoPlay controls {...props} />,
}[type.split('/')[0]];
};
return (
<Type
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
mr='sm'
src={url}
alt={'Unable to preview file'}
/>
);
}
export default function ImagesTable({
columns,
data = [],

View file

@ -1,15 +1,14 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
import useFetch from 'hooks/useFetch';
import { CheckIcon, CopyIcon, Cross1Icon, FileIcon, GearIcon, HomeIcon, Link1Icon, ResetIcon, UploadIcon, PinRightIcon, PersonIcon, Pencil1Icon, MixerHorizontalIcon } from '@modulz/radix-icons';
import { AppShell, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme, Box } from '@mantine/core';
import { AppShell, Box, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { useClipboard } from '@mantine/hooks';
import { CheckIcon, CopyIcon, Cross1Icon, FileIcon, GearIcon, HomeIcon, Link1Icon, MixerHorizontalIcon, Pencil1Icon, PersonIcon, PinRightIcon, ResetIcon, UploadIcon } from '@modulz/radix-icons';
import useFetch from 'hooks/useFetch';
import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch } from 'lib/redux/store';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) {
@ -198,7 +197,7 @@ export default function Layout({ children, user }) {
{items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref>
<UnstyledButton
sx={{
sx={{
display: 'block',
width: '100%',
padding: theme.spacing.xs,
@ -295,10 +294,13 @@ export default function Layout({ children, user }) {
<Text sx={{
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontWeight: 500,
fontSize: theme.fontSizes.xs,
fontSize: theme.fontSizes.sm,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
cursor: 'default',
}}>User: {user.username}</Text>
}}
>
{user.username}
</Text>
<MenuItemLink icon={<GearIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
<MenuItem icon={<ResetIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>

View file

@ -1,10 +1,10 @@
/// https://github.com/mui-org/material-ui/blob/next/examples/nextjs/src/Link.js
/* eslint-disable jsx-a11y/anchor-has-content */
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import NextLink from 'next/link';
import { Text } from '@mantine/core';
import clsx from 'clsx';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import { forwardRef } from 'react';
export const NextLinkComposed = forwardRef(function NextLinkComposed(props: any, ref) {
const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } =

View file

@ -0,0 +1,5 @@
import { Text } from '@mantine/core';
export default function MutedText({ children, ...props }) {
return <Text color='gray' size='xl' {...props}>{children}</Text>;
}

View file

@ -0,0 +1,29 @@
import { Box, Table } from '@mantine/core';
import { randomId } from '@mantine/hooks';
export function SmallTable({ rows, columns }) {
return (
<Box sx={{ pt: 1 }}>
<Table highlightOnHover>
<thead>
<tr>
{columns.map(col => (
<th key={randomId()}>{col.name}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr key={randomId()}>
{columns.map(col => (
<td key={randomId()}>
{col.format ? col.format(row[col.id]) : row[col.id]}
</td>
))}
</tr>
))}
</tbody>
</Table>
</Box>
);
}

View file

@ -1,6 +0,0 @@
import React from 'react';
import { Text } from '@mantine/core';
export default function StatText({ children }) {
return <Text color='gray' size='xl'>{children}</Text>;
}

View file

@ -1,22 +1,22 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';
// themes
import dark_blue from 'lib/themes/dark_blue';
import light_blue from 'lib/themes/light_blue';
import dark from 'lib/themes/dark';
import ayu_dark from 'lib/themes/ayu_dark';
import ayu_mirage from 'lib/themes/ayu_mirage';
import ayu_light from 'lib/themes/ayu_light';
import nord from 'lib/themes/nord';
import ayu_mirage from 'lib/themes/ayu_mirage';
import dark from 'lib/themes/dark';
import dark_blue from 'lib/themes/dark_blue';
import dracula from 'lib/themes/dracula';
import light_blue from 'lib/themes/light_blue';
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
import nord from 'lib/themes/nord';
import qogir_dark from 'lib/themes/qogir_dark';
import { useStoreSelector } from 'lib/redux/store';
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { NotificationsProvider } from '@mantine/notifications';
import { useColorScheme } from '@mantine/hooks';
import { useStoreSelector } from 'lib/redux/store';
export const themes = {
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,

View file

@ -0,0 +1,52 @@
import React from 'react';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { Group, Text, useMantineTheme } from '@mantine/core';
import { UploadIcon, CrossCircledIcon, ImageIcon } from '@modulz/radix-icons';
function ImageUploadIcon({ status, ...props }) {
if (status.accepted) {
return <UploadIcon {...props} />;
}
if (status.rejected) {
return <CrossCircledIcon {...props} />;
}
return <ImageIcon {...props} />;
}
function getIconColor(status, theme) {
return status.accepted
? theme.colors[theme.primaryColor][6]
: status.rejected
? theme.colors.red[6]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black;
}
export default function Dropzone({ loading, onDrop, children }) {
const theme = useMantineTheme();
return (
<MantineDropzone loading={loading} onDrop={onDrop}>
{status => (
<>
<Group position='center' spacing='xl' style={{ minHeight: 220, pointerEvents: 'none' }}>
<ImageUploadIcon
status={status}
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
/>
<Text size='xl' inline>
Drag images here or click to select files
</Text>
</Group>
{children}
</>
)}
</MantineDropzone>
);
}

View file

@ -0,0 +1,60 @@
/* eslint-disable jsx-a11y/alt-text */
import React from 'react';
import { Image, Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
export function FilePreview({ file }: { file: File }) {
const Type = props => {
return {
'video': <video autoPlay controls {...props} />,
'image': <Image withPlaceholder {...props} />,
'audio': <audio autoPlay controls {...props} />,
}[file.type.split('/')[0]];
};
return (
<Type
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)}
alt={file.name}
/>
);
}
export default function FileDropzone({ file }: { file: File }) {
const theme = useMantineTheme();
return (
<Tooltip
position='top'
placement='center'
allowPointerEvents
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
<FilePreview file={file} />
<Table sx={{ color: theme.colorScheme === 'dark' ? 'black' : 'white' }} ml='md'>
<tbody>
<tr>
<td>Name</td>
<td>{file.name}</td>
</tr>
<tr>
<td>Type</td>
<td>{file.type}</td>
</tr>
<tr>
<td>Last Modified</td>
<td>{new Date(file.lastModified).toLocaleString()}</td>
</tr>
</tbody>
</Table>
</div>
}
>
<Badge size='lg'>
{file.name}
</Badge>
</Tooltip>
);
}

View file

@ -1,33 +1,19 @@
import React, { useEffect, useState } from 'react';
import { SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
import Card from 'components/Card';
import ZiplineImage from 'components/Image';
import File from 'components/File';
import ImagesTable from 'components/ImagesTable';
import Link from 'components/Link';
import MutedText from 'components/MutedText';
import { bytesToRead } from 'lib/clientUtils';
import useFetch from 'lib/hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
import { Text, Skeleton, Title, SimpleGrid } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
import Link from 'components/Link';
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
import { useNotifications } from '@mantine/notifications';
import StatText from 'components/StatText';
import { useEffect, useState } from 'react';
type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
export function bytesToRead(bytes: number) {
if (isNaN(bytes)) return '0.0 B';
if (bytes === Infinity) return '0.0 B';
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;
while (bytes > 1024) {
bytes /= 1024;
++num;
}
return `${bytes.toFixed(1)} ${units[num]}`;
}
export default function Dashboard() {
const user = useStoreSelector(state => state.user);
@ -86,8 +72,8 @@ export default function Dashboard() {
return (
<>
<Title>Welcome back {user?.username}</Title>
<Text color='gray' sx={{ paddingBottom: 4 }}>You have <b>{images.length ? images.length : '...'}</b> files</Text>
<Title>Welcome back, {user?.username}</Title>
<Text color='gray' mb='sm'>You have <b>{images.length ? images.length : '...'}</b> files</Text>
<Title>Recent Files</Title>
<SimpleGrid
@ -98,7 +84,7 @@ export default function Dashboard() {
]}
>
{recent.length ? recent.map(image => (
<ZiplineImage key={randomId()} image={image} updateImages={updateImages} />
<File key={randomId()} image={image} updateImages={updateImages} />
)) : [1,2,3,4].map(x => (
<div key={x}>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
@ -116,20 +102,22 @@ export default function Dashboard() {
]}
>
<Card name='Size' sx={{ height: '100%' }}>
<StatText>{stats ? stats.size : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.size : <Skeleton height={8} />}</MutedText>
<Title order={2}>Average Size</Title>
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</MutedText>
</Card>
<Card name='Images' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.count : <Skeleton height={8} />}</MutedText>
<Title order={2}>Views</Title>
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</MutedText>
</Card>
<Card name='Users' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count_users : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.count_users : <Skeleton height={8} />}</MutedText>
</Card>
</SimpleGrid>
<Title mt='md'>Files</Title>
<Text>View your gallery <Link href='/dashboard/files'>here</Link>.</Text>
<ImagesTable
columns={[
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
@ -141,34 +129,6 @@ export default function Dashboard() {
copyImage={copyImage}
viewImage={viewImage}
/>
{/* <Title mt='md'>Files</Title>
<Text>View previews of your files in the <Link href='/dashboard/files'>browser</Link>.</Text>
<ReactTable
columns={[
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
{ accessor: 'mimetype', Header: 'Type', minWidth: 100, align: 'inherit' as Aligns },
{ accessor: 'created_at', Header: 'Date' },
]}
data={images}
pagination
/>
<Card name='Files per User' mt={22}>
<StatTable
columns={[
{ id: 'username', name: 'Name' },
{ id: 'count', name: 'Files' },
]}
rows={stats ? stats.count_by_user : []} />
</Card>
<Card name='Types' mt={22}>
<StatTable
columns={[
{ id: 'mimetype', name: 'Type' },
{ id: 'count', name: 'Count' },
]}
rows={stats ? stats.types_count : []} />
</Card> */}
</>
);
}

View file

@ -1,10 +1,9 @@
import React, { useEffect, useState } from 'react';
import ZiplineImage from 'components/Image';
import useFetch from 'hooks/useFetch';
import { Box, Accordion, Pagination, Title, SimpleGrid, Skeleton, Group, ActionIcon } from '@mantine/core';
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { PlusIcon } from '@modulz/radix-icons';
import File from 'components/File';
import useFetch from 'hooks/useFetch';
import Link from 'next/link';
import { useEffect, useState } from 'react';
export default function Files() {
const [pages, setPages] = useState([]);
@ -27,48 +26,50 @@ export default function Files() {
return (
<>
<Group>
<Title sx={{ marginBottom: 12 }}>Files</Title>
<Group mb='md'>
<Title>Files</Title>
<Link href='/dashboard/upload' passHref>
<ActionIcon component='a' variant='filled' color='primary'><PlusIcon/></ActionIcon>
</Link>
</Group>
<Accordion
offsetIcon={false}
sx={t => ({
marginTop: 2,
border: '1px solid',
marginBottom: 12,
borderColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0] ,
})}
>
<Accordion.Item label={<Title>Favorite Files</Title>}>
<SimpleGrid
cols={3}
spacing='lg'
breakpoints={[
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
]}
>
{favoritePages.length ? favoritePages[(favoritePage - 1) ?? 0].map(image => (
<div key={image.id}>
<ZiplineImage image={image} updateImages={() => updatePages(true)} />
</div>
)) : null}
</SimpleGrid>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 3,
}}
>
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
</Box>
</Accordion.Item>
</Accordion>
{favoritePages.length ? (
<Accordion
offsetIcon={false}
sx={t => ({
marginTop: 2,
border: '1px solid',
marginBottom: 12,
borderColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0] ,
})}
>
<Accordion.Item label={<Title>Favorite Files</Title>}>
<SimpleGrid
cols={3}
spacing='lg'
breakpoints={[
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
]}
>
{favoritePages.length ? favoritePages[(favoritePage - 1) ?? 0].map(image => (
<div key={image.id}>
<File image={image} updateImages={() => updatePages(true)} />
</div>
)) : null}
</SimpleGrid>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 3,
}}
>
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
</Box>
</Accordion.Item>
</Accordion>
) : null}
<SimpleGrid
cols={3}
spacing='lg'
@ -78,7 +79,7 @@ export default function Files() {
>
{pages.length ? pages[(page - 1) ?? 0].map(image => (
<div key={image.id}>
<ZiplineImage image={image} updateImages={() => updatePages(true)} />
<File image={image} updateImages={() => updatePages(true)} />
</div>
)) : [1,2,3,4].map(x => (
<div key={x}>

View file

@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react';
import useFetch from 'hooks/useFetch';
import Link from 'components/Link';
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip } from '@mantine/core';
import { randomId, useForm, useInterval } from '@mantine/hooks';
import { Card, Tooltip, TextInput, Button, Text, Title, Group, ColorInput, MultiSelect, Space, Box, Table } from '@mantine/core';
import { DownloadIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
import { useNotifications } from '@mantine/notifications';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { Cross1Icon, DownloadIcon, TrashIcon } from '@modulz/radix-icons';
import Link from 'components/Link';
import { SmallTable } from 'components/SmallTable';
import useFetch from 'hooks/useFetch';
import { bytesToRead } from 'lib/clientUtils';
import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
import { useEffect, useState } from 'react';
function VarsTooltip({ children }) {
return (
@ -18,7 +19,7 @@ function VarsTooltip({ children }) {
<Text><b>{'{image.mimetype}'}</b> - mimetype</Text>
<Text><b>{'{image.id}'}</b> - id of the image</Text>
<Text><b>{'{user.name}'}</b> - your username</Text>
visit <Link href='https://zipline.diced.cf/docs/variables'>the docs</Link> for more variables
visit <Link href='https://zipl.vercel.app/docs/variables'>the docs</Link> for more variables
</>
}>
{children}
@ -30,33 +31,6 @@ function ExportDataTooltip({ children }) {
return <Tooltip position='top' placement='center' color='' label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'>{children}</Tooltip>;
}
function ExportTable({ rows, columns }) {
return (
<Box sx={{ pt: 1 }} >
<Table highlightOnHover>
<thead>
<tr>
{columns.map(col => (
<th key={randomId()}>{col.name}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr key={randomId()}>
{columns.map(col => (
<td key={randomId()}>
{col.format ? col.format(row[col.id]) : row[col.id]}
</td>
))}
</tr>
))}
</tbody>
</Table>
</Box>
);
}
export default function Manage() {
const user = useStoreSelector(state => state.user);
const dispatch = useStoreDispatch();
@ -176,8 +150,9 @@ export default function Manage() {
const res = await useFetch('/api/user/export');
setExports(res.exports.map(s => ({
date: new Date(Number(s.split('_')[3].slice(0, -4))),
full: s,
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
size: s.size,
full: s.name,
})).sort((a, b) => a.date.getTime() - b.date.getTime()));
};
@ -226,7 +201,6 @@ export default function Manage() {
},
});
const interval = useInterval(() => getExports(), 30000);
useEffect(() => {
getExports();
@ -241,7 +215,7 @@ export default function Manage() {
</VarsTooltip>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} />
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
@ -258,32 +232,37 @@ export default function Manage() {
{...form.getInputProps('domains')}
/>
<Group position='right' sx={{ paddingTop: 12 }}>
<Group position='right' mt='md'>
<Button
type='submit'
>Save User</Button>
</Group>
</form>
<Title sx={{ paddingTop: 12 }}>Manage Data</Title>
<Text color='gray' sx={{ paddingBottom: 12 }}>Delete, or export your data into a zip file.</Text>
<Box mb='md'>
<Title>Manage Data</Title>
<Text color='gray'>Delete, or export your data into a zip file.</Text>
</Box>
<Group>
<Button onClick={openDeleteModal} rightIcon={<TrashIcon />}>Delete All Data</Button>
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
</Group>
<Card mt={22}>
<ExportTable
<SmallTable
columns={[
{ id: 'name', name: 'Name' },
{ id: 'date', name: 'Date' },
{ id: 'size', name: 'Size' },
]}
rows={exports ? exports.map((x, i) => ({
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
date: x.date.toLocaleString(),
size: bytesToRead(x.size),
})) : []} />
</Card>
<Title sx={{ paddingTop: 12, paddingBottom: 12 }}>ShareX Config</Title>
<Title my='md'>ShareX Config</Title>
<Group>
<Button onClick={() => genShareX(false)} rightIcon={<DownloadIcon />}>ShareX Config</Button>
<Button onClick={() => genShareX(true)} rightIcon={<DownloadIcon />}>ShareX Config with Embed</Button>

View file

@ -1,52 +1,10 @@
import React, { useEffect, useState } from 'react';
import { SimpleGrid, Skeleton, Title } from '@mantine/core';
import Card from 'components/Card';
import StatText from 'components/StatText';
import MutedText from 'components/MutedText';
import { SmallTable } from 'components/SmallTable';
import { bytesToRead } from 'lib/clientUtils';
import useFetch from 'lib/hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
import { Box, Text, Table, Skeleton, Title, SimpleGrid } from '@mantine/core';
import { randomId } from '@mantine/hooks';
export function bytesToRead(bytes: number) {
if (isNaN(bytes)) return '0.0 B';
if (bytes === Infinity) return '0.0 B';
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;
while (bytes > 1024) {
bytes /= 1024;
++num;
}
return `${bytes.toFixed(1)} ${units[num]}`;
}
function StatTable({ rows, columns }) {
return (
<Box sx={{ pt: 1 }}>
<Table highlightOnHover>
<thead>
<tr>
{columns.map(col => (
<th key={randomId()}>{col.name}</th>
))}
</tr>
</thead>
<tbody>
{rows.map(row => (
<tr key={randomId()}>
{columns.map(col => (
<td key={randomId()}>
{col.format ? col.format(row[col.id]) : row[col.id]}
</td>
))}
</tr>
))}
</tbody>
</Table>
</Box>
);
}
import { useEffect, useState } from 'react';
export default function Stats() {
const [stats, setStats] = useState(null);
@ -62,7 +20,7 @@ export default function Stats() {
return (
<>
<Title>Stats</Title>
<Title mb='md'>Stats</Title>
<SimpleGrid
cols={3}
spacing='lg'
@ -71,22 +29,22 @@ export default function Stats() {
]}
>
<Card name='Size' sx={{ height: '100%' }}>
<StatText>{stats ? stats.size : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.size : <Skeleton height={8} />}</MutedText>
<Title order={2}>Average Size</Title>
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</MutedText>
</Card>
<Card name='Images' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.count : <Skeleton height={8} />}</MutedText>
<Title order={2}>Views</Title>
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</MutedText>
</Card>
<Card name='Users' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count_users : <Skeleton height={8} />}</StatText>
<MutedText>{stats ? stats.count_users : <Skeleton height={8} />}</MutedText>
</Card>
</SimpleGrid>
<Card name='Files per User' mt={22}>
<StatTable
<SmallTable
columns={[
{ id: 'username', name: 'Name' },
{ id: 'count', name: 'Files' },
@ -94,7 +52,7 @@ export default function Stats() {
rows={stats ? stats.count_by_user : []} />
</Card>
<Card name='Types' mt={22}>
<StatTable
<SmallTable
columns={[
{ id: 'mimetype', name: 'Type' },
{ id: 'count', name: 'Count' },

View file

@ -1,83 +1,21 @@
import React, { useEffect, useState } from 'react';
import { useStoreSelector } from 'lib/redux/store';
import Link from 'components/Link';
import { Image, Button, Group, Popover, Progress, Text, useMantineTheme, Tooltip, Stack, Table } from '@mantine/core';
import { ImageIcon, UploadIcon, CrossCircledIcon } from '@modulz/radix-icons';
import { Dropzone } from '@mantine/dropzone';
import { useNotifications } from '@mantine/notifications';
import { Button, Collapse, Group, Progress, Title, useMantineTheme } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
function ImageUploadIcon({ status, ...props }) {
if (status.accepted) {
return <UploadIcon {...props} />;
}
if (status.rejected) {
return <CrossCircledIcon {...props} />;
}
return <ImageIcon {...props} />;
}
function getIconColor(status, theme) {
return status.accepted
? theme.colors[theme.primaryColor][6]
: status.rejected
? theme.colors.red[6]
: theme.colorScheme === 'dark'
? theme.colors.dark[0]
: theme.black;
}
function ImageDropzone({ file }: { file: File }) {
const theme = useMantineTheme();
return (
<Tooltip
position='top'
placement='center'
color={theme.colorScheme === 'dark' ? 'dark' : undefined}
styles={{
body: {
color: 'white',
},
}}
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
<Image src={URL.createObjectURL(file)} alt={file.name} sx={{ maxWidth: '10vw', maxHeight: '100vh' }} mr='md' />
<Table>
<tbody>
<tr>
<td>Name</td>
<td>{file.name}</td>
</tr>
<tr>
<td>Type</td>
<td>{file.type}</td>
</tr>
<tr>
<td>Last Modified</td>
<td>{new Date(file.lastModified).toLocaleString()}</td>
</tr>
</tbody>
</Table>
</div>
}
>
<Text weight='bold'>{file.name}</Text>
</Tooltip>
);
}
import { useNotifications } from '@mantine/notifications';
import { CrossCircledIcon, UploadIcon } from '@modulz/radix-icons';
import Dropzone from 'components/dropzone/Dropzone';
import FileDropzone from 'components/dropzone/DropzoneFile';
import Link from 'components/Link';
import { useStoreSelector } from 'lib/redux/store';
import { useEffect, useState } from 'react';
export default function Upload() {
const theme = useMantineTheme();
const notif = useNotifications();
const clipboard = useClipboard();
const user = useStoreSelector(state => state.user);
const [files, setFiles] = useState([]);
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
window.addEventListener('paste', (e: ClipboardEvent) => {
@ -93,6 +31,7 @@ export default function Upload() {
const handleUpload = async () => {
setProgress(0);
setLoading(true);
const body = new FormData();
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
@ -113,6 +52,7 @@ export default function Upload() {
req.addEventListener('load', e => {
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
const json = JSON.parse(e.target.response);
setLoading(false);
if (json.error === undefined) {
notif.updateNotification(id, {
@ -141,32 +81,17 @@ export default function Upload() {
return (
<>
<Dropzone onDrop={(f) => setFiles([...files, ...f])}>
{status => (
<>
<Group position='center' spacing='xl' style={{ minHeight: 220, pointerEvents: 'none' }}>
<ImageUploadIcon
status={status}
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
/>
<div>
<Text size='xl' inline>
Drag images here or click to select files
</Text>
</div>
</Group>
</>
)}
<Title mb='md'>Upload Files</Title>
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
<Group position='center' spacing='md'>
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
</Group>
</Dropzone>
<Group position='center' spacing='xl' mt={12}>
{files.map(file => (<ImageDropzone key={randomId()} file={file} />))}
</Group>
{progress !== 0 && <Progress sx={{ marginTop: 12 }} value={progress} />}
<Collapse in={progress !== 0}>
{progress !== 0 && <Progress mt='md' value={progress} animate />}
</Collapse>
<Group position='right'>
<Button leftIcon={<UploadIcon />} mt={12} onClick={handleUpload}>Upload</Button>

View file

@ -1,11 +1,10 @@
import React, { useEffect, useState } from 'react';
import { ActionIcon, Button, Card, Group, Modal, SimpleGrid, Skeleton, TextInput, Title } from '@mantine/core';
import { useClipboard, useForm } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { CopyIcon, Cross1Icon, Link1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
import useFetch from 'hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
import { useClipboard, useForm } from '@mantine/hooks';
import { CopyIcon, Cross1Icon, Link1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
import { useNotifications } from '@mantine/notifications';
import { Modal, Title, Group, Button, Box, Card, TextInput, ActionIcon, SimpleGrid, Skeleton } from '@mantine/core';
import { useEffect, useState } from 'react';
export default function Urls() {
const user = useStoreSelector(state => state.user);
@ -58,12 +57,18 @@ export default function Urls() {
},
});
const onSubmit = async (values) => {
const onSubmit = async values => {
const cleanURL = values.url.trim();
const cleanVanity = values.vanity.trim();
if (cleanURL === '') return form.setFieldError('url', 'URL can\'t be nothing');
try {
new URL(cleanURL);
} catch (e) {
return form.setFieldError('url', 'Invalid URL');
}
const data = {
url: cleanURL,
vanity: cleanVanity === '' ? null : cleanVanity,
@ -121,8 +126,8 @@ export default function Urls() {
</form>
</Modal>
<Group>
<Title sx={{ marginBottom: 12 }}>URLs</Title>
<Group mb='md'>
<Title>URLs</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon/></ActionIcon>
</Group>

View file

@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useStoreSelector } from 'lib/redux/store';
import useFetch from 'hooks/useFetch';
import { useRouter } from 'next/router';
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Switch, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/hooks';
import { Avatar, Modal, Title, TextInput, Group, Button, Card, ActionIcon, SimpleGrid, Switch, Skeleton, Checkbox } from '@mantine/core';
import { Cross1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
import { useNotifications } from '@mantine/notifications';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { Cross1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
import useFetch from 'hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
function CreateUserModal({ open, setOpen, updateUsers }) {
@ -19,7 +19,7 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
});
const notif = useNotifications();
const onSubmit = async (values) => {
const onSubmit = async values => {
const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim();
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
@ -58,7 +58,7 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
onClose={() => setOpen(false)}
title={<Title>Create User</Title>}
>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />

View file

@ -1,4 +1,4 @@
import { Image, User } from '@prisma/client';
import type { Image, User } from '@prisma/client';
export function parse(str: string, image: Image, user: User) {
if (!str) return null;
@ -13,4 +13,18 @@ export function parse(str: string, image: Image, user: User) {
.replace(/{image.created_at.full_string}/gi, image.created_at.toLocaleString())
.replace(/{image.created_at.time_string}/gi, image.created_at.toLocaleTimeString())
.replace(/{image.created_at.date_string}/gi, image.created_at.toLocaleDateString());
}
export function bytesToRead(bytes: number) {
if (isNaN(bytes)) return '0.0 B';
if (bytes === Infinity) return '0.0 B';
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;
while (bytes > 1024) {
bytes /= 1024;
++num;
}
return `${bytes.toFixed(1)} ${units[num]}`;
}

View file

@ -4,7 +4,7 @@ import parse from '@iarna/toml/parse-string';
import Logger from '../logger';
import { Config } from './Config';
const e = (val, type, fn) => ({ val, type, fn });
const e = (val, type, fn: (c: Config, v: any) => void) => ({ val, type, fn });
const envValues = [
e('SECURE', 'boolean', (c, v) => c.core.secure = v),

View file

@ -19,7 +19,7 @@ const validator = object({
s3: object({
access_key_id: string(),
secret_access_key: string(),
endpoint: string().notRequired().nullable(),
endpoint: string(),
bucket: string(),
force_s3_path: boolean().default(false),
region: string().default('us-east-1'),
@ -64,6 +64,8 @@ export default function validate(config): Config {
errors.push('datasource.s3.secret_access_key is a required field');
if (!validated.datasource.s3.bucket)
errors.push('datasource.s3.bucket is a required field');
if (!validated.datasource.s3.endpoint)
errors.push('datasource.s3.endpoint is a required field');
if (errors.length) throw { errors };
break;
}

View file

@ -6,15 +6,15 @@ if (!global.datasource) {
switch (config.datasource.type) {
case 's3':
global.datasource = new S3(config.datasource.s3);
Logger.get('datasource').info(`Using S3(${config.datasource.s3.bucket}) datasource`);
Logger.get('datasource').info(`using S3(${config.datasource.s3.bucket}) datasource`);
break;
case 'local':
global.datasource = new Local(config.datasource.local.directory);
Logger.get('datasource').info(`Using local(${config.datasource.local.directory}) datasource`);
Logger.get('datasource').info(`using Local(${config.datasource.local.directory}) datasource`);
break;
case 'swift':
global.datasource = new Swift(config.datasource.swift);
Logger.get('datasource').info(`Using Swift(${config.datasource.swift.container}) datasource`);
Logger.get('datasource').info(`using Swift(${config.datasource.swift.container}) datasource`);
break;
default:
throw new Error('Invalid datasource type');

View file

@ -1,6 +1,6 @@
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
// Popular extension map
module.exports = {
const exts = {
rb: 'ruby',
py: 'python',
pl: 'perl',
@ -35,4 +35,6 @@ module.exports = {
txt: '',
coffee: 'coffee',
swift: 'swift',
};
};
export default exts;

View file

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import type { CookieSerializeOptions } from 'cookie';
import { serialize } from 'cookie';
import { sign64, unsign64 } from '../util';
import { sign64, unsign64 } from 'lib/util';
import config from 'lib/config';
import prisma from 'lib/prisma';
@ -36,7 +36,7 @@ export type NextApiRes = NextApiResponse & {
error: (message: string) => void;
forbid: (message: string, extra?: any) => void;
bad: (message: string) => void;
json: (json: any) => void;
json: (json: Record<string, any>) => void;
ratelimited: (remaining: number) => void;
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
}

View file

@ -1,4 +1,4 @@
module.exports = {
const mimes = {
'.aac': 'audio/aac',
'.abw': 'application/x-abiword',
'.arc': 'application/x-freearc',
@ -75,4 +75,6 @@ module.exports = {
'.3gp': 'video/3gpp',
'.3g2': 'video/3gpp2',
'.7z': 'application/x-7z-compressed',
};
};
export default mimes;

View file

@ -2,9 +2,8 @@ import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
import { hash, verify } from 'argon2';
import { readdir, stat } from 'fs/promises';
import { join } from 'path';
import prisma from './prisma';
import prisma from 'lib/prisma';
import { InvisibleImage, InvisibleUrl } from '@prisma/client';
import config from './config';
export async function hashPassword(s: string): Promise<string> {
return await hash(s);

View file

@ -1,19 +1,25 @@
import React from 'react';
import { Box, Text } from '@mantine/core';
import { Box, Button, Stack, Text, Title } from '@mantine/core';
import Link from 'components/Link';
import MutedText from 'components/MutedText';
export default function FourOhFour() {
return (
<>
<Box
<Stack
sx={{
display: 'flex',
alignItems: 'center',
minHeight: '100vh',
justifyContent: 'center',
position: 'relative',
}}
spacing='sm'
>
<Text size='xl'>404 - Not Found</Text>
</Box>
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>404</Title>
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>This page does not exist!</MutedText>
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
</Stack>
</>
);
}

25
src/pages/500.tsx Normal file
View file

@ -0,0 +1,25 @@
import React from 'react';
import { Button, Stack, Title } from '@mantine/core';
import Link from 'components/Link';
import MutedText from 'components/MutedText';
export default function FiveHundred() {
return (
<>
<Stack
sx={{
display: 'flex',
alignItems: 'center',
minHeight: '100vh',
justifyContent: 'center',
position: 'relative',
}}
spacing='sm'
>
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>500</Title>
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Internal Server Error</MutedText>
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
</Stack>
</>
);
}

View file

@ -5,7 +5,7 @@ import { Box, Button, Modal, PasswordInput } from '@mantine/core';
import config from 'lib/config';
import prisma from 'lib/prisma';
import { parse } from 'lib/clientUtils';
import * as exts from '../../scripts/exts';
import exts from 'lib/exts';
export default function EmbeddedImage({ image, user, pass }) {
const dataURL = (route: string) => `${route}/${image.file}`;

View file

@ -9,7 +9,6 @@ export default function MyApp({ Component, pageProps }) {
<Provider store={store}>
<Head>
<title>{Component.title}</title>
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
</Head>
<ZiplineTheming Component={Component} pageProps={pageProps} />
</Provider>

30
src/pages/_error.tsx Normal file
View file

@ -0,0 +1,30 @@
import React from 'react';
import { Button, Stack, Title } from '@mantine/core';
import Link from 'components/Link';
import MutedText from 'components/MutedText';
export default function Error({ statusCode }) {
return (
<>
<Stack
sx={{
display: 'flex',
alignItems: 'center',
minHeight: '100vh',
justifyContent: 'center',
position: 'relative',
}}
spacing='sm'
>
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>{statusCode}</Title>
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Something went wrong...</MutedText>
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
</Stack>
</>
);
}
export function getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { pageProps: { statusCode } };
}

View file

@ -2,7 +2,7 @@ import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
import { checkPassword } from 'lib/util';
import datasource from 'lib/datasource';
import mimes from '../../../../scripts/mimes';
import mimes from 'lib/mimes';
import { extname } from 'path';
async function handler(req: NextApiReq, res: NextApiRes) {

View file

@ -6,8 +6,8 @@ import { createInvisImage, randomChars, hashPassword } from 'lib/util';
import Logger from 'lib/logger';
import { ImageFormat, InvisibleImage } from '@prisma/client';
import { format as formatDate } from 'fecha';
import { v4 } from 'uuid';
import datasource from 'lib/datasource';
import { randomUUID } from 'crypto';
const uploader = multer();
@ -63,7 +63,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
fileName = formatDate(new Date(), 'YYYY-MM-DD_HH:mm:ss');
break;
case ImageFormat.UUID:
fileName = v4();
fileName = randomUUID({ disableEntropyCache: true });
break;
case ImageFormat.NAME:
fileName = file.originalname.split('.')[0];

View file

@ -3,7 +3,7 @@ import prisma from 'lib/prisma';
import Logger from 'lib/logger';
import { Zip, ZipPassThrough } from 'fflate';
import datasource from 'lib/datasource';
import { readdir } from 'fs/promises';
import { readdir, stat } from 'fs/promises';
import { createReadStream, createWriteStream } from 'fs';
async function handler(req: NextApiReq, res: NextApiRes) {
@ -21,6 +21,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
const write_stream = createWriteStream(`/tmp/${export_name}`);
// i found this on some stack overflow thing, forgot the url
const onBackpressure = (stream, outputStream, cb) => {
const runCb = () => {
// Pause if either output or internal backpressure should be applied
@ -85,7 +86,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}
};
// for (const file of files) {
Logger.get('user').info(`Export for ${user.username} (${user.id}) has started`);
for (let i = 0; i !== files.length; ++i) {
const file = files[i];
@ -123,7 +123,15 @@ async function handler(req: NextApiReq, res: NextApiRes) {
stream.pipe(res);
} else {
const files = await readdir('/tmp');
const exports = files.filter(f => f.startsWith('zipline_export_'));
const exp = files.filter(f => f.startsWith('zipline_export_'));
const exports = [];
for (let i = 0; i !== exp.length; ++i) {
const name = exp[i];
const stats = await stat(`/tmp/${name}`);
exports.push({ name, size: stats.size });
}
res.json({
exports,
});

View file

@ -1,14 +1,11 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import useFetch from 'hooks/useFetch';
import { Button, Center, TextInput, Title, PasswordInput } from '@mantine/core';
import { useForm } from '@mantine/hooks';
import { TextInput, Button, Center, Title, Box, Badge, Tooltip } from '@mantine/core';
import { useNotifications } from '@mantine/notifications';
import { Cross1Icon, DownloadIcon } from '@modulz/radix-icons';
import useFetch from 'hooks/useFetch';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function Login() {
const router = useRouter();
const notif = useNotifications();
const form = useForm({
initialValues: {
@ -28,12 +25,12 @@ export default function Login() {
});
if (res.error) {
notif.showNotification({
title: 'Login Failed',
message: res.error,
color: 'red',
icon: <Cross1Icon />,
});
if (res.error.startsWith('403')) {
form.setFieldError('password', 'Invalid password');
} else {
form.setFieldError('username', 'Invalid username');
form.setFieldError('password', 'Invalid password');
}
} else {
await router.push(router.query.url as string || '/dashboard');
}
@ -53,7 +50,7 @@ export default function Login() {
<Title align='center'>Zipline</Title>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput size='lg' id='username' label='Username' {...form.getInputProps('username')} />
<TextInput size='lg' id='password' label='Password' type='password' {...form.getInputProps('password')} />
<PasswordInput size='lg' id='password' label='Password' {...form.getInputProps('password')} />
<Button size='lg' type='submit' fullWidth mt={12}>Login</Button>
</form>

View file

@ -1,8 +1,11 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import { LoadingOverlay } from '@mantine/core';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
export default function Logout() {
const dispatch = useStoreDispatch();
const router = useRouter();
useEffect(() => {
@ -10,7 +13,10 @@ export default function Logout() {
const userRes = await fetch('/api/user');
if (userRes.ok) {
const res = await fetch('/api/auth/logout');
if (res.ok) router.push('/auth/login');
if (res.ok) {
dispatch(updateUser(null));
router.push('/auth/login');
}
} else {
router.push('/auth/login');
}

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import exts from '../../../scripts/exts';
import exts from 'lib/exts';
import { Prism } from '@mantine/prism';
export default function Code() {

View file

@ -2,11 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Files from 'components/pages/Files';
import { LoadingOverlay } from '@mantine/core';
export default function FilesPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout
@ -17,4 +18,4 @@ export default function FilesPage() {
);
}
FilesPage.title = 'Zipline - Gallery';
FilesPage.title = 'Zipline - Files';

View file

@ -2,10 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Dashboard from 'components/pages/Dashboard';
import { LoadingOverlay } from '@mantine/core';
export default function DashboardPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -2,11 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Manage from 'components/pages/Manage';
import { LoadingOverlay } from '@mantine/core';
export default function ManagePage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -2,10 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Stats from 'components/pages/Stats';
import { LoadingOverlay } from '@mantine/core';
export default function StatsPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -2,11 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Upload from 'components/pages/Upload';
import { LoadingOverlay } from '@mantine/core';
export default function UploadPage({ route }) {
export default function UploadPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -2,11 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Urls from 'components/pages/Urls';
import { LoadingOverlay } from '@mantine/core';
export default function UrlsPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -2,11 +2,12 @@ import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Users from 'components/pages/Users';
import { LoadingOverlay } from '@mantine/core';
export default function UsersPage() {
const { user, loading } = useLogin();
if (loading) return null;
if (loading) return <LoadingOverlay visible={loading} />;
return (
<Layout

View file

@ -7,11 +7,13 @@ import { extname } from 'path';
import { mkdir } from 'fs/promises';
import { getStats, log, migrations } from './util';
import Logger from '../lib/logger';
import mimes from '../../scripts/mimes';
import exts from '../../scripts/exts';
import mimes from '../lib/mimes';
import exts from '../lib/exts';
import { version } from '../../package.json';
import config from '../lib/config';
import datasource from '../lib/datasource';
import type { Config } from 'lib/config/Config';
import type { Datasource } from 'lib/datasources';
let config: Config, datasource: Datasource;
const logger = Logger.get('server');
logger.info(`starting zipline@${version} server`);
@ -19,6 +21,13 @@ logger.info(`starting zipline@${version} server`);
start();
async function start() {
const c = await import('../lib/config.js');
config = c.default.default;
const d = await import('../lib/datasource.js');
// @ts-ignore
datasource = d.default.default;
// annoy user if they didnt change secret from default "changethis"
if (config.core.secret === 'changethis') {
logger.error('Secret is not set!');
@ -64,7 +73,6 @@ async function start() {
],
},
});
console.log(image);
if (!image) await rawFile(req, res, nextServer, params.id);
@ -102,7 +110,7 @@ async function start() {
});
http.on('listening', () => {
logger.info(`Listening on ${config.core.host}:${config.core.port}`);
logger.info(`listening on ${config.core.host}:${config.core.port}`);
});
http.listen(config.core.port, config.core.host ?? '0.0.0.0');

1920
yarn.lock

File diff suppressed because it is too large Load diff