feat: configurable invites & disable_media_preview config

This commit is contained in:
diced 2022-10-02 15:39:59 -07:00
parent ec0e7e5ec7
commit d31371eb6c
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
19 changed files with 72 additions and 20 deletions

View file

@ -5,6 +5,7 @@
"scripts": { "scripts": {
"dev": "cross-env REACT_EDITOR=code NODE_ENV=development tsx src/server", "dev": "cross-env REACT_EDITOR=code NODE_ENV=development tsx src/server",
"build": "npm-run-all build:schema build:next", "build": "npm-run-all build:schema build:next",
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
"build:next": "next build", "build:next": "next build",
"build:schema": "prisma generate --schema=prisma/schema.prisma", "build:schema": "prisma generate --schema=prisma/schema.prisma",
"migrate:dev": "prisma migrate dev --create-only", "migrate:dev": "prisma migrate dev --create-only",
@ -79,4 +80,4 @@
"url": "https://github.com/diced/zipline.git" "url": "https://github.com/diced/zipline.git"
}, },
"packageManager": "yarn@3.2.1" "packageManager": "yarn@3.2.1"
} }

View file

@ -4,9 +4,10 @@ import { showNotification } from '@mantine/notifications';
import { relativeTime } from 'lib/utils/client'; import { relativeTime } from 'lib/utils/client';
import { useFileDelete, useFileFavorite } from 'lib/queries/files'; import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useState } from 'react'; import { useState } from 'react';
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons'; import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon, FileIcon, HashIcon, ImageIcon, StarIcon } from './icons';
import MutedText from './MutedText'; import MutedText from './MutedText';
import Type from './Type'; import Type from './Type';
import Link from './Link';
export function FileMeta({ Icon, title, subtitle, ...other }) { export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? ( return other.tooltip ? (
@ -30,7 +31,7 @@ export function FileMeta({ Icon, title, subtitle, ...other }) {
); );
} }
export default function File({ image, updateImages }) { export default function File({ image, updateImages, disableMediaPreview }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const deleteFile = useFileDelete(); const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite(); const favoriteFile = useFileFavorite();
@ -112,6 +113,7 @@ export default function File({ image, updateImages }) {
popup popup
sx={{ minHeight: 200 }} sx={{ minHeight: 200 }}
style={{ minHeight: 200 }} style={{ minHeight: 200 }}
disableMediaPreview={false}
/> />
<Stack> <Stack>
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} /> <FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
@ -128,7 +130,10 @@ export default function File({ image, updateImages }) {
</Stack> </Stack>
<Group position='right' mt={22}> <Group position='right' mt={22}>
<Button onClick={handleCopy}>Copy</Button> <Link href={image.url} target='_blank'>
<Button rightIcon={<ExternalLinkIcon />}>Open</Button>
</Link>
<Button onClick={handleCopy}>Copy URL</Button>
<Button onClick={handleDelete}>Delete</Button> <Button onClick={handleDelete}>Delete</Button>
<Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button> <Button onClick={handleFavorite}>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</Group> </Group>
@ -143,6 +148,7 @@ export default function File({ image, updateImages }) {
src={image.url} src={image.url}
alt={image.file} alt={image.file}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/> />
</Card.Section> </Card.Section>
</Card> </Card>

View file

@ -101,15 +101,18 @@ const admin_items = [
icon: <UserIcon size={18} />, icon: <UserIcon size={18} />,
text: 'Users', text: 'Users',
link: '/dashboard/users', link: '/dashboard/users',
if: props => true,
}, },
{ {
icon: <TagIcon size={18} />, icon: <TagIcon size={18} />,
text: 'Invites', text: 'Invites',
link: '/dashboard/invites', link: '/dashboard/invites',
if: props => props.invites,
}, },
]; ];
export default function Layout({ children, props }) { export default function Layout({ children, props }) {
console.log(props);
const [user, setUser] = useRecoilState(userSelector); const [user, setUser] = useRecoilState(userSelector);
const { title } = props; const { title } = props;
@ -230,7 +233,7 @@ export default function Layout({ children, props }) {
childrenOffset={28} childrenOffset={28}
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)} defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
> >
{admin_items.map(({ icon, text, link }) => ( {admin_items.filter(x => x.if(props)).map(({ icon, text, link }) => (
<Link href={link} key={text} passHref> <Link href={link} key={text} passHref>
<NavLink <NavLink
component='a' component='a'

View file

@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, PlayIcon } from './icons'; import { AudioIcon, FileIcon, PlayIcon } from './icons';
function Placeholder({ text, Icon, ...props }) { function Placeholder({ text, Icon, ...props }) {
if (props.disableResolve) props.src = null;
return ( return (
<Image height={200} withPlaceholder placeholder={ <Image height={200} withPlaceholder placeholder={
<Group> <Group>
@ -14,7 +16,7 @@ function Placeholder({ text, Icon, ...props }) {
); );
} }
export default function Type({ file, popup = false, ...props }){ export default function Type({ file, popup = false, disableMediaPreview, ...props }){
const type = (file.type || file.mimetype).split('/')[0]; const type = (file.type || file.mimetype).split('/')[0];
const name = (file.name || file.file); const name = (file.name || file.file);
@ -33,6 +35,10 @@ export default function Type({ file, popup = false, ...props }){
}, []); }, []);
} }
if (media && disableMediaPreview) {
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />;
};
return popup ? (media ? { return popup ? (media ? {
'video': <video width='100%' autoPlay controls {...props} />, 'video': <video width='100%' autoPlay controls {...props} />,
'image': <Image {...props} />, 'image': <Image {...props} />,

View file

@ -11,6 +11,7 @@ export function FilePreview({ file }: { file: File }) {
style={{ maxWidth: '10vw', maxHeight: '100vh' }} style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)} src={URL.createObjectURL(file)}
alt={file.name} alt={file.name}
disableMediaPreview={false}
popup popup
/> />
); );

View file

@ -5,7 +5,7 @@ import MutedText from 'components/MutedText';
import { invalidateFiles, useRecent } from 'lib/queries/files'; import { invalidateFiles, useRecent } from 'lib/queries/files';
import { UploadCloud } from 'react-feather'; import { UploadCloud } from 'react-feather';
export default function RecentFiles() { export default function RecentFiles({ disableMediaPreview }) {
const recent = useRecent('media'); const recent = useRecent('media');
return ( return (
@ -24,7 +24,7 @@ export default function RecentFiles() {
recent.data.length > 0 recent.data.length > 0
? ( ? (
recent.data.map(image => ( recent.data.map(image => (
<File key={randomId()} image={image} updateImages={invalidateFiles} /> <File key={randomId()} image={image} updateImages={invalidateFiles} disableMediaPreview={disableMediaPreview} />
)) ))
) : ( ) : (
<MantineCard shadow='md'> <MantineCard shadow='md'>

View file

@ -13,7 +13,7 @@ import { useRecoilValue } from 'recoil';
import RecentFiles from './RecentFiles'; import RecentFiles from './RecentFiles';
import { StatCards } from './StatCards'; import { StatCards } from './StatCards';
export default function Dashboard() { export default function Dashboard({ disableMediaPreview }) {
const user = useRecoilValue(userSelector); const user = useRecoilValue(userSelector);
const theme = useMantineTheme(); const theme = useMantineTheme();
@ -69,7 +69,7 @@ export default function Dashboard() {
<StatCards /> <StatCards />
<RecentFiles /> <RecentFiles disableMediaPreview={disableMediaPreview} />
<section> <section>
<Title>Files</Title> <Title>Files</Title>

View file

@ -5,7 +5,7 @@ import MutedText from 'components/MutedText';
import { usePaginatedFiles } from 'lib/queries/files'; import { usePaginatedFiles } from 'lib/queries/files';
import { useState } from 'react'; import { useState } from 'react';
export default function FilePagation() { export default function FilePagation({ disableMediaPreview }) {
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const pages = usePaginatedFiles(!checked ? { filter: 'media' } : {}); const pages = usePaginatedFiles(!checked ? { filter: 'media' } : {});
@ -42,7 +42,7 @@ export default function FilePagation() {
? ( ? (
pages.data[(page - 1) ?? 0].map(image => ( pages.data[(page - 1) ?? 0].map(image => (
<div key={image.id}> <div key={image.id}>
<File image={image} updateImages={() => pages.refetch()} /> <File image={image} updateImages={() => pages.refetch()} disableMediaPreview={disableMediaPreview} />
</div> </div>
)) ))
) : ( ) : (

View file

@ -6,7 +6,7 @@ import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
import FilePagation from './FilePagation'; import FilePagation from './FilePagation';
export default function Files() { export default function Files({ disableMediaPreview }) {
const pages = usePaginatedFiles({ filter: 'media' }); const pages = usePaginatedFiles({ filter: 'media' });
const favoritePages = usePaginatedFiles({ favorite: 'media' }); const favoritePages = usePaginatedFiles({ favorite: 'media' });
const [favoritePage, setFavoritePage] = useState(1); const [favoritePage, setFavoritePage] = useState(1);
@ -46,7 +46,7 @@ export default function Files() {
> >
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => ( {(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
<div key={image.id}> <div key={image.id}>
<File image={image} updateImages={() => updatePages(true)} /> <File image={image} updateImages={() => updatePages(true)} disableMediaPreview={disableMediaPreview} />
</div> </div>
)) : null} )) : null}
</SimpleGrid> </SimpleGrid>
@ -59,7 +59,7 @@ export default function Files() {
paddingBottom: 3, paddingBottom: 3,
}} }}
> >
<Pagination total={favoritePages.data.length} page={favoritePage} onChange={setFavoritePage}/> <Pagination total={favoritePages.data.length} page={favoritePage} onChange={setFavoritePage} />
</Box> </Box>
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
@ -67,7 +67,7 @@ export default function Files() {
) : null ) : null
} }
<FilePagation /> <FilePagation disableMediaPreview={disableMediaPreview} />
</> </>
); );
} }

View file

@ -63,6 +63,7 @@ export interface ConfigWebsite {
title: string; title: string;
show_files_per_user: boolean; show_files_per_user: boolean;
show_version: boolean; show_version: boolean;
disable_media_preview: boolean;
external_links: ConfigWebsiteExternalLinks[]; external_links: ConfigWebsiteExternalLinks[];
} }
@ -96,6 +97,10 @@ export interface ConfigDiscordEmbed {
image: boolean; image: boolean;
} }
export interface ConfigFeatures {
invites: boolean;
}
export interface Config { export interface Config {
core: ConfigCore; core: ConfigCore;
uploader: ConfigUploader; uploader: ConfigUploader;
@ -104,4 +109,5 @@ export interface Config {
datasource: ConfigDatasource; datasource: ConfigDatasource;
website: ConfigWebsite; website: ConfigWebsite;
discord: ConfigDiscord; discord: ConfigDiscord;
features: ConfigFeatures;
} }

View file

@ -86,6 +86,7 @@ export default function readConfig() {
map('WEBSITE_TITLE', 'string', 'website.title'), map('WEBSITE_TITLE', 'string', 'website.title'),
map('WEBSITE_SHOW_FILES_PER_USER', 'boolean', 'website.show_files_per_user'), map('WEBSITE_SHOW_FILES_PER_USER', 'boolean', 'website.show_files_per_user'),
map('WEBSITE_SHOW_VERSION', 'boolean', 'website.show_version'), map('WEBSITE_SHOW_VERSION', 'boolean', 'website.show_version'),
map('WEBSITE_DISABLE_MEDIA_PREVIEW', 'boolean', 'website.disable_media_preview'),
map('WEBSITE_EXTERNAL_LINKS', 'json-array', 'website.external_links'), map('WEBSITE_EXTERNAL_LINKS', 'json-array', 'website.external_links'),
map('DISCORD_URL', 'string', 'discord.url'), map('DISCORD_URL', 'string', 'discord.url'),
@ -109,6 +110,8 @@ export default function readConfig() {
map('DISCORD_SHORTEN_EMBED_IMAGE', 'boolean', 'discord.shorten.embed.image'), map('DISCORD_SHORTEN_EMBED_IMAGE', 'boolean', 'discord.shorten.embed.image'),
map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'), map('DISCORD_SHORTEN_EMBED_THUMBNAIL', 'boolean', 'discord.shorten.embed.thumbnail'),
map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'), map('DISCORD_SHORTEN_EMBED_TIMESTAMP', 'boolean', 'discord.shorten.embed.timestamp'),
map('FEATURES_INVITES', 'boolean', 'features.invites'),
]; ];
const config = {}; const config = {};

View file

@ -69,6 +69,8 @@ const validator = object({
title: string().default('Zipline'), title: string().default('Zipline'),
show_files_per_user: boolean().default(true), show_files_per_user: boolean().default(true),
show_version: boolean().default(true), show_version: boolean().default(true),
disable_media_preview: boolean().default(false),
external_links: array(object({ external_links: array(object({
label: string(), label: string(),
link: string(), link: string(),
@ -84,6 +86,9 @@ const validator = object({
upload: discord_content, upload: discord_content,
shorten: discord_content, shorten: discord_content,
}).optional().nullable().default(null), }).optional().nullable().default(null),
features: object({
invites: boolean().default(true),
}).required(),
}); });
export default function validate(config): Config { export default function validate(config): Config {

View file

@ -1,11 +1,13 @@
import config from 'lib/config'; import config from 'lib/config';
import { GetServerSideProps } from 'next'; import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async context => { export const getServerSideProps: GetServerSideProps = async () => {
return { return {
props: { props: {
title: config.website.title, title: config.website.title,
external_links: JSON.stringify(config.website.external_links), external_links: JSON.stringify(config.website.external_links),
disable_media_preview: config.website.disable_media_preview,
invites: config.features.invites,
}, },
}; };
}; };

View file

@ -2,9 +2,12 @@ import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline'; import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
import { createToken, hashPassword } from 'lib/util'; import { createToken, hashPassword } from 'lib/util';
import Logger from 'lib/logger'; import Logger from 'lib/logger';
import config from 'lib/config';
async function handler(req: NextApiReq, res: NextApiRes) { async function handler(req: NextApiReq, res: NextApiRes) {
if (req.method === 'POST' && req.body && req.body.code) { if (req.method === 'POST' && req.body && req.body.code) {
if (!config.features.invites) return res.forbid('invites are disabled');
const { code, username, password } = req.body as { code: string; username: string, password: string }; const { code, username, password } = req.body as { code: string; username: string, password: string };
const invite = await prisma.invite.findUnique({ const invite = await prisma.invite.findUnique({
where: { code }, where: { code },

View file

@ -2,8 +2,11 @@ import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline'; import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
import { randomChars } from 'lib/util'; import { randomChars } from 'lib/util';
import Logger from 'lib/logger'; import Logger from 'lib/logger';
import config from 'lib/config';
async function handler(req: NextApiReq, res: NextApiRes) { async function handler(req: NextApiReq, res: NextApiRes) {
if (!config.features.invites) return res.forbid('invites are disabled');
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');
if (!user.administrator) return res.forbid('you arent an administrator'); if (!user.administrator) return res.forbid('you arent an administrator');

View file

@ -20,7 +20,7 @@ export default function FilesPage(props) {
<Layout <Layout
props={props} props={props}
> >
<Files /> <Files disableMediaPreview={props.disable_media_preview} />
</Layout> </Layout>
</> </>
); );

View file

@ -19,7 +19,7 @@ export default function DashboardPage(props) {
<Layout <Layout
props={props} props={props}
> >
<Dashboard /> <Dashboard disableMediaPreview={props.disable_media_preview} />
</Layout> </Layout>
</> </>
); );

View file

@ -1,12 +1,21 @@
import React from 'react'; import React, { useEffect } from 'react';
import useLogin from 'hooks/useLogin'; import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout'; import Layout from 'components/Layout';
import Invites from 'components/pages/Invites'; import Invites from 'components/pages/Invites';
import { LoadingOverlay } from '@mantine/core'; import { LoadingOverlay } from '@mantine/core';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router';
export { getServerSideProps } from 'middleware/getServerSideProps'; export { getServerSideProps } from 'middleware/getServerSideProps';
export default function InvitesPage(props) { export default function InvitesPage(props) {
const router = useRouter();
if (!props.invites) {
useEffect(() => {
router.push('/dashboard');
}, []);
return null;
};
const { loading } = useLogin(); const { loading } = useLogin();
if (loading) return <LoadingOverlay visible={loading} />; if (loading) return <LoadingOverlay visible={loading} />;

View file

@ -132,6 +132,10 @@ export default function Invite({ code, title }) {
} }
export const getServerSideProps: GetServerSideProps = async context => { export const getServerSideProps: GetServerSideProps = async context => {
if (!config.features.invites) return {
notFound: true,
};
const { code } = context.query as { code: string }; const { code } = context.query as { code: string };
const invite = await prisma.invite.findUnique({ const invite = await prisma.invite.findUnique({