feat(v3.6.0-rc1): small fixes
This commit is contained in:
parent
642e8796f0
commit
a90130e8bf
14 changed files with 73 additions and 84 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "zipline",
|
"name": "zipline",
|
||||||
"version": "3.5.1",
|
"version": "3.6.0-rc1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm-run-all build:server dev:run",
|
"dev": "npm-run-all build:server dev:run",
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
"docker:run": "docker-compose up -d",
|
"docker:run": "docker-compose up -d",
|
||||||
"docker:down": "docker-compose down",
|
"docker:down": "docker-compose down",
|
||||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
||||||
"scripts:read-config": "node dist/scripts/read-config"
|
"scripts:read-config": "npm-run-all build:server && node dist/scripts/read-config"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dicedtomato/mantine-data-grid": "0.0.23",
|
"@dicedtomato/mantine-data-grid": "0.0.23",
|
||||||
|
@ -82,4 +82,4 @@
|
||||||
"url": "https://github.com/diced/zipline.git"
|
"url": "https://github.com/diced/zipline.git"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.4"
|
"packageManager": "yarn@3.2.4"
|
||||||
}
|
}
|
|
@ -21,6 +21,7 @@ import {
|
||||||
Image,
|
Image,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
|
Menu,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
|
@ -194,7 +195,7 @@ export default function Layout({ children, props }) {
|
||||||
|
|
||||||
const openResetToken = () =>
|
const openResetToken = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Reset Token',
|
title: <Title>Reset Token?</Title>,
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
Once you reset your token, you will have to update any uploaders to use this new token.
|
Once you reset your token, you will have to update any uploaders to use this new token.
|
||||||
|
@ -227,7 +228,7 @@ export default function Layout({ children, props }) {
|
||||||
|
|
||||||
const openCopyToken = () =>
|
const openCopyToken = () =>
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Copy Token',
|
title: <Title>Copy Token</Title>,
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
Make sure you don't share this token with anyone as they will be able to upload files on your
|
Make sure you don't share this token with anyone as they will be able to upload files on your
|
||||||
|
@ -362,17 +363,10 @@ export default function Layout({ children, props }) {
|
||||||
|
|
||||||
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Text
|
<Menu.Label>
|
||||||
sx={{
|
{user.username}{' '}
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
|
||||||
fontWeight: 500,
|
</Menu.Label>
|
||||||
fontSize: theme.fontSizes.sm,
|
|
||||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
|
||||||
cursor: 'default',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{user.username}
|
|
||||||
</Text>
|
|
||||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
|
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
|
||||||
Manage Account
|
Manage Account
|
||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
|
@ -398,20 +392,10 @@ export default function Layout({ children, props }) {
|
||||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>
|
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>
|
||||||
Logout
|
Logout
|
||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
<Divider
|
<Menu.Divider />
|
||||||
variant='solid'
|
|
||||||
my={theme.spacing.xs / 2}
|
|
||||||
sx={(theme) => ({
|
|
||||||
width: '110%',
|
|
||||||
borderTopColor:
|
|
||||||
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
|
||||||
margin: `${theme.spacing.xs / 2}px -4px`,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{user.oauth ? (
|
{user.oauth ? (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
noClick
|
|
||||||
icon={
|
icon={
|
||||||
user.oauthProvider === 'discord' ? (
|
user.oauthProvider === 'discord' ? (
|
||||||
<DiscordIcon size={18} />
|
<DiscordIcon size={18} />
|
||||||
|
@ -424,16 +408,7 @@ export default function Layout({ children, props }) {
|
||||||
<span style={{ textTransform: 'capitalize' }}>{user.oauthProvider}</span>
|
<span style={{ textTransform: 'capitalize' }}>{user.oauthProvider}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<Divider
|
<Menu.Divider />
|
||||||
variant='solid'
|
|
||||||
my={theme.spacing.xs / 2}
|
|
||||||
sx={(theme) => ({
|
|
||||||
width: '110%',
|
|
||||||
borderTopColor:
|
|
||||||
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
|
||||||
margin: `${theme.spacing.xs / 2}px -4px`,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem icon={<PencilIcon />}>
|
<MenuItem icon={<PencilIcon />}>
|
||||||
|
|
|
@ -100,6 +100,11 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loader: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: 'dots',
|
||||||
|
},
|
||||||
|
},
|
||||||
Card: {
|
Card: {
|
||||||
styles: (t) => ({
|
styles: (t) => ({
|
||||||
root: {
|
root: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
|
import { Table, Tooltip, Badge, HoverCard, Text, useMantineTheme, Group } from '@mantine/core';
|
||||||
import Type from 'components/Type';
|
import Type from 'components/Type';
|
||||||
|
|
||||||
export function FilePreview({ file }: { file: File }) {
|
export function FilePreview({ file }: { file: File }) {
|
||||||
|
@ -21,10 +21,12 @@ export default function FileDropzone({ file }: { file: File }) {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<HoverCard shadow='md'>
|
||||||
position='top'
|
<HoverCard.Target>
|
||||||
label={
|
<Badge size='lg'>{file.name}</Badge>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown>
|
||||||
|
<Group grow>
|
||||||
<FilePreview file={file} />
|
<FilePreview file={file} />
|
||||||
|
|
||||||
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : 'white' }} ml='md'>
|
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : 'white' }} ml='md'>
|
||||||
|
@ -43,10 +45,8 @@ export default function FileDropzone({ file }: { file: File }) {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</Group>
|
||||||
}
|
</HoverCard.Dropdown>
|
||||||
>
|
</HoverCard>
|
||||||
<Badge size='lg'>{file.name}</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal opened={opened} onClose={onClose} title={<Title order={3}>{title}</Title>} size='lg'>
|
<Modal opened={opened} onClose={onClose} title={<Title order={3}>{title}</Title>} size='lg'>
|
||||||
{other.desc && <Text>{other.desc}</Text>}
|
{other.desc && <Text mb='md'>{other.desc}</Text>}
|
||||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||||
<Select
|
<Select
|
||||||
label='Select file name format'
|
label='Select file name format'
|
||||||
|
|
|
@ -283,21 +283,28 @@ export default function Manage() {
|
||||||
<Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables
|
<Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables
|
||||||
</MutedText>
|
</MutedText>
|
||||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
<TextInput id='username' label='Username' my='sm' {...form.getInputProps('username')} />
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
id='password'
|
id='password'
|
||||||
label='Password'
|
label='Password'
|
||||||
description='Leave blank to keep your old password'
|
description='Leave blank to keep your old password'
|
||||||
|
my='sm'
|
||||||
{...form.getInputProps('password')}
|
{...form.getInputProps('password')}
|
||||||
/>
|
/>
|
||||||
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
|
<TextInput id='embedTitle' label='Embed Title' my='sm' {...form.getInputProps('embedTitle')} />
|
||||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
<ColorInput id='embedColor' label='Embed Color' my='sm' {...form.getInputProps('embedColor')} />
|
||||||
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
|
<TextInput
|
||||||
|
id='embedSiteName'
|
||||||
|
label='Embed Site Name'
|
||||||
|
my='sm'
|
||||||
|
{...form.getInputProps('embedSiteName')}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='domains'
|
id='domains'
|
||||||
label='Domains'
|
label='Domains'
|
||||||
description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.'
|
description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.'
|
||||||
placeholder='https://example.com, https://example2.com'
|
placeholder='https://example.com, https://example2.com'
|
||||||
|
my='sm'
|
||||||
{...form.getInputProps('domains')}
|
{...form.getInputProps('domains')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ export default function Upload() {
|
||||||
dropdownPosition='top'
|
dropdownPosition='top'
|
||||||
data={Object.keys(exts).map((x) => ({ value: x, label: exts[x] }))}
|
data={Object.keys(exts).map((x) => ({ value: x, label: exts[x] }))}
|
||||||
icon={<TypeIcon />}
|
icon={<TypeIcon />}
|
||||||
|
searchable
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<UploadIcon />}
|
leftIcon={<UploadIcon />}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import type { CookieSerializeOptions } from 'cookie';
|
import type { CookieSerializeOptions } from 'cookie';
|
||||||
|
|
||||||
import { serialize } from 'cookie';
|
import { serialize } from 'cookie';
|
||||||
import { sign64, unsign64 } from 'lib/util';
|
import { sign64, unsign64 } from 'lib/utils/crypto';
|
||||||
import config from 'lib/config';
|
import config from 'lib/config';
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import { User } from '@prisma/client';
|
import { User } from '@prisma/client';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { hash, verify } from 'argon2';
|
import { hash, verify } from 'argon2';
|
||||||
import { readdir, stat } from 'fs/promises';
|
import { readdir, stat } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
@ -25,31 +25,6 @@ export function createToken() {
|
||||||
return randomChars(24) + '.' + Buffer.from(Date.now().toString()).toString('base64url');
|
return randomChars(24) + '.' + Buffer.from(Date.now().toString()).toString('base64url');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sign(value: string, secret: string): string {
|
|
||||||
const signed = value + ':' + createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '');
|
|
||||||
|
|
||||||
return signed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unsign(value: string, secret: string): string {
|
|
||||||
const str = value.slice(0, value.lastIndexOf(':'));
|
|
||||||
|
|
||||||
const mac = sign(str, secret);
|
|
||||||
|
|
||||||
const macBuffer = Buffer.from(mac);
|
|
||||||
const valBuffer = Buffer.from(value);
|
|
||||||
|
|
||||||
return timingSafeEqual(macBuffer, valBuffer) ? str : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sign64(value: string, secret: string): string {
|
|
||||||
return Buffer.from(sign(value, secret)).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unsign64(value: string, secret: string): string {
|
|
||||||
return unsign(Buffer.from(value, 'base64').toString(), secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chunk<T>(arr: T[], size: number): Array<T[]> {
|
export function chunk<T>(arr: T[], size: number): Array<T[]> {
|
||||||
const result = [];
|
const result = [];
|
||||||
const L = arr.length;
|
const L = arr.length;
|
||||||
|
|
26
src/lib/utils/crypto.ts
Normal file
26
src/lib/utils/crypto.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { createHmac, timingSafeEqual } from 'crypto';
|
||||||
|
|
||||||
|
export function sign(value: string, secret: string): string {
|
||||||
|
const signed = value + ':' + createHmac('sha256', secret).update(value).digest('base64').replace(/=+$/, '');
|
||||||
|
|
||||||
|
return signed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsign(value: string, secret: string): string {
|
||||||
|
const str = value.slice(0, value.lastIndexOf(':'));
|
||||||
|
|
||||||
|
const mac = sign(str, secret);
|
||||||
|
|
||||||
|
const macBuffer = Buffer.from(mac);
|
||||||
|
const valBuffer = Buffer.from(value);
|
||||||
|
|
||||||
|
return timingSafeEqual(macBuffer, valBuffer) ? str : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sign64(value: string, secret: string): string {
|
||||||
|
return Buffer.from(sign(value, secret)).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsign64(value: string, secret: string): string {
|
||||||
|
return unsign(Buffer.from(value, 'base64').toString(), secret);
|
||||||
|
}
|
|
@ -7,12 +7,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
const user = await req.user();
|
const user = await req.user();
|
||||||
if (!user) return res.forbid('not logged in');
|
if (!user) return res.forbid('not logged in');
|
||||||
|
|
||||||
let amount = typeof req.query.amount === 'string' ? parseInt(req.query.amount) : 2;
|
let amount = typeof req.query.amount === 'string' ? Number(req.query.amount) : 2;
|
||||||
if (isNaN(amount)) return res.bad('invalid amount');
|
if (isNaN(amount)) return res.bad('invalid amount');
|
||||||
|
|
||||||
// get stats per day
|
// get stats per day
|
||||||
|
|
||||||
var stats = await prisma.$queryRaw<Stats[]>`
|
let stats: Stats[] = await prisma.$queryRaw`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "Stats" as t JOIN
|
FROM "Stats" as t JOIN
|
||||||
(SELECT MAX(t2."created_at") as max_timestamp
|
(SELECT MAX(t2."created_at") as max_timestamp
|
||||||
|
|
|
@ -24,7 +24,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return res.forbid('authorization incorect');
|
if (!user) return res.forbid('authorization incorrect');
|
||||||
if (user.ratelimit) {
|
if (user.ratelimit) {
|
||||||
const remaining = user.ratelimit.getTime() - Date.now();
|
const remaining = user.ratelimit.getTime() - Date.now();
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
|
|
|
@ -87,7 +87,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
Logger.get('user').info(`Export for ${user.username} (${user.id}) has started`);
|
Logger.get('user').info(`Export for ${user.username} (${user.id}) has started`);
|
||||||
for (let i = 0; i !== files.length; ++i) {
|
for (let i = 0; i !== files.length; ++i) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
const stream = await datasource.get(file.file);
|
const stream = datasource.get(file.file);
|
||||||
if (stream) {
|
if (stream) {
|
||||||
const def = new ZipPassThrough(file.file);
|
const def = new ZipPassThrough(file.file);
|
||||||
zip.add(def);
|
zip.add(def);
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import config from 'lib/config';
|
import config from '../lib/config';
|
||||||
|
|
||||||
console.log(JSON.stringify(config, null, 2));
|
console.log(JSON.stringify(config, null, 2));
|
||||||
|
|
Loading…
Reference in a new issue