Merge branch 'trunk' into feature/oauth-authentik

This commit is contained in:
dicedtomato 2023-05-06 08:05:44 -10:00 committed by GitHub
commit 61b65700a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 184 additions and 168 deletions

View file

@ -3,7 +3,7 @@
# if using s3/supabase make sure to comment out the other datasources
CORE_HTTPS=true
CORE_RETURN_HTTPS=true
CORE_SECRET="changethis"
CORE_HOST=0.0.0.0
CORE_PORT=3000
@ -44,3 +44,5 @@ URLS_LENGTH=6
RATELIMIT_USER=5
RATELIMIT_ADMIN=3
# for more variables checkout the docs

View file

@ -114,7 +114,7 @@ model OAuth {
id Int @id @default(autoincrement())
provider OauthProviders
user User @relation(fields: [userId], references: [uuid], onDelete: Cascade)
userId String
userId String @db.Uuid
username String
oauthId String?
token String

View file

@ -95,18 +95,12 @@ export default function FileModal({
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
setOpen(false);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
};
const handleFavorite = async () => {

View file

@ -4,10 +4,8 @@ import {
Box,
Burger,
Button,
Group,
Header,
Image,
Input,
MediaQuery,
Menu,
Navbar,
@ -222,21 +220,14 @@ export default function Layout({ children, props }) {
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: (
<Text size='sm'>
Zipline is unable to copy to clipboard due to security reasons. However, you can still copy
the token manually.
<br />
<Group position='left' spacing='sm'>
<Text>Your token is:</Text>
<Input size='sm' onFocus={(e) => e.target.select()} type='text' value={token} />
</Group>
</Text>
),
title: 'Unable to copy token',
message:
"Zipline couldn't copy to your clipboard. Please copy the token manually from the settings page.",
color: 'red',
icon: <IconClipboardCopy size='1rem' />,
});
else
showNotification({

View file

@ -106,22 +106,16 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
const copyFile = async (file) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: (
<a
href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
),
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: (
<a
href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
),
icon: <IconClipboardCopy size='1rem' />,
});
};
const viewFile = async (file) => {

View file

@ -40,6 +40,12 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
const pages = usePaginatedFiles(page, !checked ? 'media' : null);
if (pages.isSuccess && pages.data.length === 0) {
if (page > 1 && numPages > 0) {
setPage(page - 1);
return null;
}
return (
<Center sx={{ flexDirection: 'column' }}>
<Group>

View file

@ -112,7 +112,7 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
const makePublic = async (folder) => {
const res = await useFetch(`/api/user/folders/${folder.id}`, 'PATCH', {
public: folder.public ? false : true,
public: !folder.public,
});
if (!res.error) {
@ -363,25 +363,18 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
aria-label='copy link'
onClick={() => {
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied folder link',
message: (
<>
Copied{' '}
<AnchorNext href={`/folder/${folder.id}`}>folder link</AnchorNext> to
clipboard
</>
),
color: 'green',
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied folder link',
message: (
<>
Copied <AnchorNext href={`/folder/${folder.id}`}>folder link</AnchorNext>{' '}
to clipboard
</>
),
color: 'green',
icon: <IconClipboardCopy size='1rem' />,
});
}}
>
<IconClipboardCopy size='1rem' />

View file

@ -184,18 +184,12 @@ export default function Invites() {
const handleCopy = async (invite) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}/auth/register?code=${invite.code}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
};
const updateInvites = async () => {

View file

@ -1,9 +1,11 @@
import {
ActionIcon,
Anchor,
Box,
Button,
Card,
ColorInput,
CopyButton,
FileInput,
Group,
Image,
@ -23,6 +25,8 @@ import {
IconBrandDiscordFilled,
IconBrandGithubFilled,
IconBrandGoogle,
IconCheck,
IconClipboardCopy,
IconFileExport,
IconFiles,
IconFilesOff,
@ -91,6 +95,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
const [file, setFile] = useState<File | null>(null);
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret);
const [tokenShown, setTokenShown] = useState(false);
const getDataURL = (f: File): Promise<string> => {
return new Promise((res, rej) => {
@ -367,6 +372,25 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<AnchorNext href='https://zipline.diced.tech/docs/guides/variables'>the docs</AnchorNext> for
variables
</MutedText>
<TextInput
rightSection={
<CopyButton value={user.token} timeout={1000}>
{({ copied, copy }) => (
<ActionIcon onClick={copy}>
{copied ? <IconCheck color='green' size='1rem' /> : <IconClipboardCopy size='1rem' />}
</ActionIcon>
)}
</CopyButton>
}
// @ts-ignore (this works even though ts doesn't allow for it)
component='span'
label='Token'
onClick={() => setTokenShown(true)}
>
{tokenShown ? user.token : '[click to reveal]'}
</TextInput>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' my='sm' {...form.getInputProps('username')} />
<PasswordInput

View file

@ -27,18 +27,12 @@ export default function MetadataView({ fileId }) {
const copy = (value) => {
clipboard.copy(value);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: value,
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: value,
icon: <IconClipboardCopy size='1rem' />,
});
};
const searchValue = (value) => {

View file

@ -7,18 +7,12 @@ export default function showFilesModal(clipboard, modals, files: string[]) {
const open = (idx: number) => window.open(files[idx], '_blank');
const copy = (idx: number) => {
clipboard.copy(files[idx]);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: <AnchorNext href={files[idx]}>{files[idx]}</AnchorNext>,
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: <AnchorNext href={files[idx]}>{files[idx]}</AnchorNext>,
icon: <IconClipboardCopy size='1rem' />,
});
};
modals.openModal({

View file

@ -169,18 +169,12 @@ export default function Urls() {
const copyURL = (u) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
};
const urlDelete = useURLDelete();

View file

@ -123,6 +123,8 @@ export interface ConfigFeatures {
}
export interface ConfigOAuth {
bypass_local_login: boolean;
github_client_id?: string;
github_client_secret?: string;

View file

@ -136,6 +136,8 @@ export default function readConfig() {
map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'),
map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'),
map('OAUTH_BYPASS_LOCAL_LOGIN', 'boolean', 'oauth.bypass_local_login'),
map('OAUTH_GITHUB_CLIENT_ID', 'string', 'oauth.github_client_id'),
map('OAUTH_GITHUB_CLIENT_SECRET', 'string', 'oauth.github_client_secret'),

View file

@ -168,6 +168,8 @@ const validator = s.object({
.nullish.default(null),
oauth: s
.object({
bypass_local_login: s.boolean.default(false),
github_client_id: s.string.nullable.default(null),
github_client_secret: s.string.nullable.default(null),

View file

@ -50,22 +50,22 @@ export class S3 extends Datasource {
}
public size(file: string): Promise<number> {
return new Promise((res, rej) => {
return new Promise((res) => {
this.s3.statObject(this.config.bucket, file, (err, stat) => {
if (err) rej(err);
if (err) res(0);
else res(stat.size);
});
});
}
public async fullSize(): Promise<number> {
return new Promise((res, rej) => {
return new Promise((res) => {
const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
let size = 0;
objects.on('data', (item) => (size += item.size));
objects.on('end', (err) => {
if (err) rej(err);
if (err) res(0);
else res(size);
});
});

View file

@ -2,6 +2,7 @@ import date from './date';
import gfycat from './gfycat';
import random from './random';
import uuid from './uuid';
import { parse } from 'path';
export type NameFormat = 'random' | 'date' | 'uuid' | 'name' | 'gfycat';
export const NameFormats: NameFormat[] = ['random', 'date', 'uuid', 'name', 'gfycat'];
@ -14,7 +15,9 @@ export default async function formatFileName(nameFormat: NameFormat, originalNam
case 'uuid':
return uuid();
case 'name':
return originalName.split('.')[0];
const { name } = parse(originalName);
return name;
case 'gfycat':
return gfycat();
default:

View file

@ -16,6 +16,7 @@ export type ServerSideProps = {
user_registration: boolean;
oauth_registration: boolean;
oauth_providers: string;
bypass_local_login: boolean;
chunks_size: number;
max_size: number;
totp_enabled: boolean;
@ -74,6 +75,7 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
user_registration: config.features.user_registration,
oauth_registration: config.features.oauth_registration,
oauth_providers: JSON.stringify(oauth_providers),
bypass_local_login: config.oauth.bypass_local_login,
chunks_size: config.chunks.chunks_size,
max_size: config.chunks.max_size,
totp_enabled: config.mfa.totp_enabled,

View file

@ -7,6 +7,7 @@ import { extname } from 'path';
async function handler(req: NextApiReq, res: NextApiRes) {
const { id, password } = req.query;
if (isNaN(Number(id))) return res.badRequest('invalid id');
const file = await prisma.file.findFirst({
where: {

View file

@ -12,7 +12,7 @@ import { createInvisImage, hashPassword } from 'lib/util';
import { parseExpiry } from 'lib/utils/client';
import { removeGPSData } from 'lib/utils/exif';
import multer from 'multer';
import { join } from 'path';
import { join, parse } from 'path';
import sharp from 'sharp';
import { Worker } from 'worker_threads';
@ -200,7 +200,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
let mimetype = file.mimetype;
if (file.mimetype === 'application/octet-stream' && zconfig.uploader.assume_mimetypes) {
const ext = file.originalname.split('.').pop();
const ext = parse(file.originalname).ext.replace('.', '');
const mime = await guess(ext);
if (!mime) response.assumed_mimetype = false;

View file

@ -1,4 +1,4 @@
import config from 'lib/config';
import zconfig from 'lib/config';
import Logger from 'lib/logger';
import { authentik_auth, discord_auth, github_auth, google_auth } from 'lib/oauth';
import prisma from 'lib/prisma';
@ -18,7 +18,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json({
error: 'oauth token expired',
redirect_uri: github_auth.oauth_url(config.oauth.github_client_id),
redirect_uri: github_auth.oauth_url(zconfig.oauth.github_client_id),
});
}
} else if (user.oauth.find((o) => o.provider === 'DISCORD')) {
@ -35,8 +35,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json({
error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url(
config.oauth.discord_client_id,
`${config.core.return_https ? 'https' : 'http'}://${req.headers.host}`
zconfig.oauth.discord_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
),
});
}
@ -47,8 +47,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: config.oauth.discord_client_id,
client_secret: config.oauth.discord_client_secret,
client_id: zconfig.oauth.discord_client_id,
client_secret: zconfig.oauth.discord_client_secret,
grant_type: 'refresh_token',
refresh_token: provider.refresh,
}),
@ -59,8 +59,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json({
error: 'oauth token expired',
redirect_uri: discord_auth.oauth_url(
config.oauth.discord_client_id,
`${config.core.return_https ? 'https' : 'http'}://${req.headers.host}`
zconfig.oauth.discord_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
),
});
}
@ -90,8 +90,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json({
error: 'oauth token expired',
redirect_uri: google_auth.oauth_url(
config.oauth.google_client_id,
`${config.core.return_https ? 'https' : 'http'}://${req.headers.host}`
zconfig.oauth.google_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
),
});
}
@ -101,8 +101,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: config.oauth.google_client_id,
client_secret: config.oauth.google_client_secret,
client_id: zconfig.oauth.google_client_id,
client_secret: zconfig.oauth.google_client_secret,
grant_type: 'refresh_token',
refresh_token: provider.refresh,
}),
@ -113,8 +113,8 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json({
error: 'oauth token expired',
redirect_uri: google_auth.oauth_url(
config.oauth.google_client_id,
`${config.core.return_https ? 'https' : 'http'}://${req.headers.host}`
zconfig.oauth.google_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`
),
});
}
@ -258,6 +258,14 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
}
}
export const config = {
api: {
bodyParser: {
sizeLimit: '50mb',
},
},
};
export default withZipline(handler, {
methods: ['GET', 'PATCH'],
user: true,

View file

@ -22,7 +22,13 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
export { getServerSideProps } from 'middleware/getServerSideProps';
export default function Login({ title, user_registration, oauth_registration, oauth_providers: unparsed }) {
export default function Login({
title,
user_registration,
oauth_registration,
bypass_local_login,
oauth_providers: unparsed,
}) {
const router = useRouter();
// totp modal
@ -34,6 +40,9 @@ export default function Login({ title, user_registration, oauth_registration, oa
const oauth_providers = JSON.parse(unparsed);
const show_local_login =
router.query.local === 'true' || !(bypass_local_login && oauth_providers?.length > 0);
const icons = {
GitHub: IconBrandGithub,
Discord: IconBrandDiscordFilled,
@ -100,6 +109,12 @@ export default function Login({ title, user_registration, oauth_registration, oa
useEffect(() => {
(async () => {
// if the user includes `local=true` as a query param, show the login form
// otherwise, redirect to the oauth login if there is only one registered provider
if (bypass_local_login && oauth_providers?.length === 1 && router.query.local !== 'true') {
await router.push(oauth_providers[0].url);
}
const a = await fetch('/api/user');
if (a.ok) await router.push('/dashboard');
})();
@ -153,7 +168,7 @@ export default function Login({ title, user_registration, oauth_registration, oa
<Center sx={{ height: '100vh' }}>
<Card radius='md'>
<Title size={30} align='left'>
{title}
{bypass_local_login ? ' Login to Zipline with' : 'Zipline'}
</Title>
{oauth_registration && (
@ -166,7 +181,7 @@ export default function Login({ title, user_registration, oauth_registration, oa
variant='outline'
radius='md'
fullWidth
leftIcon={<Icon height={'15'} width={'15'} />}
leftIcon={<Icon size='1rem' />}
my='xs'
component={Link}
href={url}
@ -175,41 +190,42 @@ export default function Login({ title, user_registration, oauth_registration, oa
</Button>
))}
</Group>
<Divider my='xs' label='or' labelPosition='center' />
{show_local_login && <Divider my='xs' label='or' labelPosition='center' />}
</>
)}
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput
my='xs'
radius='md'
size='md'
id='username'
label='Username'
{...form.getInputProps('username')}
/>
<PasswordInput
my='xs'
radius='md'
size='md'
id='password'
label='Password'
{...form.getInputProps('password')}
/>
{show_local_login && (
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput
my='xs'
radius='md'
size='md'
id='username'
label='Username'
{...form.getInputProps('username')}
/>
<PasswordInput
my='xs'
radius='md'
size='md'
id='password'
label='Password'
{...form.getInputProps('password')}
/>
<Group position='apart'>
{user_registration && (
<Anchor size='xs' href='/auth/register' component={Link}>
Don&apos;t have an account? Register
</Anchor>
)}
<Group position='apart'>
{user_registration && (
<Anchor size='xs' href='/auth/register' component={Link}>
Don&apos;t have an account? Register
</Anchor>
)}
<Button size='sm' p='xs' radius='md' my='xs' type='submit' loading={loading}>
Login
</Button>
</Group>
</form>
<Button size='sm' p='xs' radius='md' my='xs' type='submit' loading={loading}>
Login
</Button>
</Group>
</form>
)}
</Card>
</Center>
</>

View file

@ -231,7 +231,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
if (file.password) file.password = true;
return {
props: {
image: file,
file,
user,
pass,
prismRender: true,

View file

@ -29,7 +29,7 @@ async function main() {
for (let i = 0; i !== files.length; ++i) {
const file = files[i];
if (!datasource.get(file.name)) {
if (!(await datasource.get(file.name))) {
if (process.argv.includes('--force-delete')) {
console.log(`File ${file.name} does not exist. Deleting...`);
await prisma.file.delete({