feat: add version to appshell

This commit is contained in:
diced 2022-08-24 20:37:57 -07:00
parent 1d42d922bd
commit 45541a3cdd
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
13 changed files with 152 additions and 156 deletions

View file

@ -25,6 +25,7 @@
- Discord embeds (OG metadata) - Discord embeds (OG metadata)
- Gallery viewer, and multiple file format support - Gallery viewer, and multiple file format support
- Code highlighting - Code highlighting
- Fully customizable Discord webhook notifications
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`) - Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
# Usage # Usage

View file

@ -1,6 +1,6 @@
{ {
"name": "zipline", "name": "zipline",
"version": "3.5.0", "version": "3.4.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "REACT_EDITOR=code NODE_ENV=development tsx src/server", "dev": "REACT_EDITOR=code NODE_ENV=development tsx src/server",
@ -15,7 +15,7 @@
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build" "docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
}, },
"dependencies": { "dependencies": {
"@dicedtomato/mantine-data-grid": "0.0.20", "@dicedtomato/mantine-data-grid": "0.0.21",
"@emotion/react": "^11.9.3", "@emotion/react": "^11.9.3",
"@emotion/server": "^11.4.0", "@emotion/server": "^11.4.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",

View file

@ -1,4 +1,4 @@
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group, Image } from '@mantine/core'; import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group, Image, Tooltip, Badge } from '@mantine/core';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals'; import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
@ -7,7 +7,7 @@ import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch } from 'lib/redux/store'; import { useStoreDispatch } from 'lib/redux/store';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons'; import { ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons';
import { friendlyThemeName, themes } from './Theming'; import { friendlyThemeName, themes } from './Theming';
@ -111,10 +111,11 @@ const admin_items = [
export default function Layout({ children, user, title }) { export default function Layout({ children, user, title }) {
const [token, setToken] = useState(user?.token); const [token, setToken] = useState(user?.token);
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system'); const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
const [avatar, setAvatar] = useState(user.avatar ?? null); const [version, setVersion] = useState<{ local: string, upstream: string }>(null);
const [opened, setOpened] = useState(false); // navigation open const [opened, setOpened] = useState(false); // navigation open
const [open, setOpen] = useState(false); // manage acc dropdown const [open, setOpen] = useState(false); // manage acc dropdown
const avatar = user?.avatar ?? null;
const router = useRouter(); const router = useRouter();
const dispatch = useStoreDispatch(); const dispatch = useStoreDispatch();
const theme = useMantineTheme(); const theme = useMantineTheme();
@ -191,6 +192,15 @@ export default function Layout({ children, user, title }) {
}, },
}); });
useEffect(() => {
(async () => {
const data = await useFetch('/api/version');
if (!data.error) {
setVersion(data);
}
})();
}, []);
return ( return (
<AppShell <AppShell
navbarOffsetBreakpoint='sm' navbarOffsetBreakpoint='sm'
@ -238,6 +248,27 @@ export default function Layout({ children, user, title }) {
</NavLink> </NavLink>
)} )}
</Navbar.Section> </Navbar.Section>
{version ? (
<Navbar.Section>
<Tooltip
label={
version.local !== version.upstream
? `You are running an outdated version of Zipline, refer to the docs on how to update to ${version.upstream}`
: 'You are running the latest version of Zipline'
}
>
<Badge
m='md'
radius='md'
size='lg'
variant='dot'
color={version.local !== version.upstream ? 'red' : 'primary'}
>
{version.local}
</Badge>
</Tooltip>
</Navbar.Section>
) : null}
</Navbar> </Navbar>
} }
header={ header={

View file

@ -116,77 +116,75 @@ export default function Dashboard() {
<Title mt='md'>Files</Title> <Title mt='md'>Files</Title>
<MutedText size='md'>View your gallery <Link href='/dashboard/files'>here</Link>.</MutedText> <MutedText size='md'>View your gallery <Link href='/dashboard/files'>here</Link>.</MutedText>
<Box> <DataGrid
<DataGrid data={images}
data={images} loading={images.length ? false : true}
loading={images.length ? false : true} withPagination={true}
withPagination={true} withColumnResizing={false}
withColumnResizing={false} withColumnFilters={true}
withColumnFilters={true} noEllipsis={true}
noEllipsis={true} withSorting={true}
withSorting={true} highlightOnHover={true}
highlightOnHover={true} CopyIcon={CopyIcon}
CopyIcon={CopyIcon} DeleteIcon={DeleteIcon}
DeleteIcon={DeleteIcon} EnterIcon={EnterIcon}
EnterIcon={EnterIcon} deleteImage={deleteImage}
deleteImage={deleteImage} copyImage={copyImage}
copyImage={copyImage} viewImage={viewImage}
viewImage={viewImage} styles={{
styles={{ dataCell: {
dataCell: { width: '100%',
width: '100%', },
td: {
':nth-child(1)': {
minWidth: 170,
}, },
td: { ':nth-child(2)': {
':nth-child(1)': { minWidth: 100,
minWidth: 170,
},
':nth-child(2)': {
minWidth: 100,
},
}, },
th: { },
':nth-child(1)': { th: {
minWidth: 170, ':nth-child(1)': {
padding: theme.spacing.lg, minWidth: 170,
borderTopLeftRadius: theme.radius.sm, padding: theme.spacing.lg,
}, borderTopLeftRadius: theme.radius.sm,
':nth-child(2)': {
minWidth: 100,
padding: theme.spacing.lg,
},
':nth-child(3)': {
padding: theme.spacing.lg,
},
':nth-child(4)': {
padding: theme.spacing.lg,
borderTopRightRadius: theme.radius.sm,
},
}, },
thead: { ':nth-child(2)': {
backgroundColor: theme.colors.dark[6], minWidth: 100,
padding: theme.spacing.lg,
}, },
}} ':nth-child(3)': {
empty={<></>} padding: theme.spacing.lg,
},
':nth-child(4)': {
padding: theme.spacing.lg,
borderTopRightRadius: theme.radius.sm,
},
},
thead: {
backgroundColor: theme.colors.dark[6],
},
}}
empty={<></>}
columns={[ columns={[
{ {
accessorKey: 'file', accessorKey: 'file',
header: 'Name', header: 'Name',
filterFn: stringFilterFn, filterFn: stringFilterFn,
}, },
{ {
accessorKey: 'mimetype', accessorKey: 'mimetype',
header: 'Type', header: 'Type',
filterFn: stringFilterFn, filterFn: stringFilterFn,
}, },
{ {
accessorKey: 'created_at', accessorKey: 'created_at',
header: 'Date', header: 'Date',
filterFn: dateFilterFn, filterFn: dateFilterFn,
}, },
]} ]}
/> />
</Box>
</> </>
); );
} }

View file

@ -1,63 +1,32 @@
export interface ConfigCore { export interface ConfigCore {
// Whether to return http or https links
https: boolean; https: boolean;
// Used for signing of cookies and other stuff
secret: string; secret: string;
// The host Zipline will run on
host: string; host: string;
// The port Zipline will run on
port: number; port: number;
// The PostgreSQL database url
database_url: string; database_url: string;
// Whether or not to log stuff
logger: boolean; logger: boolean;
// The interval to store stats
stats_interval: number; stats_interval: number;
invites_interval: number;
} }
export interface ConfigDatasource { export interface ConfigDatasource {
// The type of datasource
type: 'local' | 's3' | 'swift'; type: 'local' | 's3' | 'swift';
// The local datasource, the default
local: ConfigLocalDatasource; local: ConfigLocalDatasource;
// The s3 datasource
s3?: ConfigS3Datasource; s3?: ConfigS3Datasource;
// The Swift datasource
swift?: ConfigSwiftDatasource; swift?: ConfigSwiftDatasource;
} }
export interface ConfigLocalDatasource { export interface ConfigLocalDatasource {
// The directory to store files in
directory: string; directory: string;
} }
export interface ConfigS3Datasource { export interface ConfigS3Datasource {
// The access key id for the s3 bucket
access_key_id: string; access_key_id: string;
// The secret access key for the s3 bucket
secret_access_key: string; secret_access_key: string;
// Not required, but if using a non-aws S3 service you can specify the endpoint
endpoint?: string; endpoint?: string;
// The S3 bucket to store files in
bucket: string; bucket: string;
// If true Zipline will attempt to connect to the bucket via the url "https://s3.amazonaws.com/{bucket}/stuff"
// If false Zipline will attempt to connect to the bucket via the url "http://{bucket}.s3.amazonaws.com/stuff"
force_s3_path: boolean; force_s3_path: boolean;
// Region
// aws region, default will be us-east-1 (if using a non-aws S3 service this might work for you)
region?: string; region?: string;
} }
@ -72,44 +41,27 @@ export interface ConfigSwiftDatasource {
} }
export interface ConfigUploader { export interface ConfigUploader {
// The route uploads will be served on
route: string; route: string;
// Length of random chars to generate for file names
length: number; length: number;
// Admin file upload limit
admin_limit: number; admin_limit: number;
// User file upload limit
user_limit: number; user_limit: number;
// Disabled extensions to block from uploading
disabled_extensions: string[]; disabled_extensions: string[];
} }
export interface ConfigUrls { export interface ConfigUrls {
// The route urls will be served on
route: string; route: string;
// Length of random chars to generate for urls
length: number; length: number;
} }
// Ratelimiting for users/admins, setting them to 0 disables ratelimiting
export interface ConfigRatelimit { export interface ConfigRatelimit {
// Ratelimit for users
user: number; user: number;
// Ratelimit for admins
admin: number; admin: number;
} }
export interface ConfigWebsite { export interface ConfigWebsite {
// Change the title from Zipline to something else
title: string; title: string;
// If zipline should show files per user in the stats page
show_files_per_user: boolean; show_files_per_user: boolean;
show_version: boolean;
} }
export interface ConfigDiscord { export interface ConfigDiscord {

View file

@ -49,6 +49,7 @@ export default function readConfig() {
map('CORE_DATABASE_URL', 'string', 'core.database_url'), map('CORE_DATABASE_URL', 'string', 'core.database_url'),
map('CORE_LOGGER', 'boolean', 'core.logger'), map('CORE_LOGGER', 'boolean', 'core.logger'),
map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'), map('CORE_STATS_INTERVAL', 'number', 'core.stats_interval'),
map('CORE_INVITES_INTERVAL', 'number', 'core.invites_interval'),
map('DATASOURCE_TYPE', 'string', 'datasource.type'), map('DATASOURCE_TYPE', 'string', 'datasource.type'),
@ -83,6 +84,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('DISCORD_URL', 'string', 'discord.url'), map('DISCORD_URL', 'string', 'discord.url'),
map('DISCORD_USERNAME', 'string', 'discord.username'), map('DISCORD_USERNAME', 'string', 'discord.username'),

View file

@ -23,6 +23,7 @@ const validator = object({
database_url: string().required(), database_url: string().required(),
logger: boolean().default(false), logger: boolean().default(false),
stats_interval: number().default(1800), stats_interval: number().default(1800),
invites_interval: number().default(1800),
}).required(), }).required(),
datasource: object({ datasource: object({
type: string().oneOf(['local', 's3', 'swift']).default('local'), type: string().oneOf(['local', 's3', 'swift']).default('local'),
@ -66,6 +67,7 @@ const validator = object({
website: object({ website: 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),
}), }),
discord: object({ discord: object({
url: string(), url: string(),
@ -110,8 +112,6 @@ export default function validate(config): Config {
} }
} }
console.log(validated);
return validated as unknown as Config; return validated as unknown as Config;
} catch (e) { } catch (e) {
if (process.env.ZIPLINE_DOCKER_BUILD) return null; if (process.env.ZIPLINE_DOCKER_BUILD) return null;

View file

@ -114,8 +114,6 @@ export async function sendShorten(user: User, url: Url, host: string) {
}] : null, }] : null,
}; };
console.log(body);
const res = await fetch(config.discord.url, { const res = await fetch(config.discord.url, {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),

View file

@ -1,8 +1,14 @@
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import config from 'lib/config';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
async function handler(req: NextApiReq, res: NextApiRes) { async function handler(req: NextApiReq, res: NextApiRes) {
const user = await req.user();
if (!user) return res.forbid('not logged in');
if (!config.website.show_version) return res.bad('version hidden');
const pkg = JSON.parse(await readFile('package.json', 'utf8')); const pkg = JSON.parse(await readFile('package.json', 'utf8'));
const re = await fetch('https://raw.githubusercontent.com/diced/zipline/trunk/package.json'); const re = await fetch('https://raw.githubusercontent.com/diced/zipline/trunk/package.json');

View file

@ -142,15 +142,7 @@ export const getServerSideProps: GetServerSideProps = async context => {
if (!invite) return { notFound: true }; if (!invite) return { notFound: true };
if (invite.used) return { notFound: true }; if (invite.used) return { notFound: true };
if (invite.expires_at && invite.expires_at < new Date()) { if (invite.expires_at && invite.expires_at < new Date()) return { notFound: true };
await prisma.invite.delete({
where: {
code,
},
});
return { notFound: true };
};
return { return {
props: { props: {

View file

@ -115,6 +115,16 @@ async function start() {
logger.info(`started ${dev ? 'development' : 'production'} zipline@${version} server`); logger.info(`started ${dev ? 'development' : 'production'} zipline@${version} server`);
stats(prisma); stats(prisma);
setInterval(async () => {
await prisma.invite.deleteMany({
where: {
used: true,
},
});
if (config.core.logger) logger.info('invites cleaned');
}, config.core.invites_interval * 1000);
} }
async function rawFile( async function rawFile(

View file

@ -5,23 +5,29 @@ import { Datasource } from 'lib/datasources';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
export async function migrations() { export async function migrations() {
const migrate = new Migrate('./prisma/schema.prisma'); try {
await ensureDatabaseExists('apply', true, './prisma/schema.prisma'); const migrate = new Migrate('./prisma/schema.prisma');
await ensureDatabaseExists('apply', true, './prisma/schema.prisma');
const diagnose = await migrate.diagnoseMigrationHistory({ const diagnose = await migrate.diagnoseMigrationHistory({
optInToShadowDatabase: false, optInToShadowDatabase: false,
}); });
if (diagnose.history?.diagnostic === 'databaseIsBehind') { if (diagnose.history?.diagnostic === 'databaseIsBehind') {
try { try {
Logger.get('database').info('migrating database'); Logger.get('database').info('migrating database');
await migrate.applyMigrations(); await migrate.applyMigrations();
} finally { } finally {
migrate.stop();
Logger.get('database').info('finished migrating database');
}
} else {
migrate.stop(); migrate.stop();
Logger.get('database').info('finished migrating database');
} }
} else { } catch (error) {
migrate.stop(); Logger.get('database').error('Failed to migrate database... exiting...');
Logger.get('database').error(error);
process.exit(1);
} }
} }

View file

@ -301,9 +301,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@dicedtomato/mantine-data-grid@npm:0.0.20": "@dicedtomato/mantine-data-grid@npm:0.0.21":
version: 0.0.20 version: 0.0.21
resolution: "@dicedtomato/mantine-data-grid@npm:0.0.20" resolution: "@dicedtomato/mantine-data-grid@npm:0.0.21"
dependencies: dependencies:
"@emotion/react": ^11.9.3 "@emotion/react": ^11.9.3
"@mantine/core": ^5.0.0 "@mantine/core": ^5.0.0
@ -324,7 +324,7 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
dayjs: dayjs:
optional: true optional: true
checksum: 6d23048cc4d0dba093c8bbbaa70f7f7b8539834bcf11f070669b54a643b8def70c21a3a3ed7e807220e2c9928239c658b0a8f717a466cbfd4e8933576a2762a2 checksum: bca215db4cc2fbf776a30c2fe9fbb2e95649d9adfd8b014c04ae6a8dff00a726c6042546d79144f11323e2895539cc5d83686d9e5c6bc7a2e9f1a48cb2831b87
languageName: node languageName: node
linkType: hard linkType: hard
@ -8867,7 +8867,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "zipline@workspace:." resolution: "zipline@workspace:."
dependencies: dependencies:
"@dicedtomato/mantine-data-grid": 0.0.20 "@dicedtomato/mantine-data-grid": 0.0.21
"@emotion/react": ^11.9.3 "@emotion/react": ^11.9.3
"@emotion/server": ^11.4.0 "@emotion/server": ^11.4.0
"@iarna/toml": 2.2.5 "@iarna/toml": 2.2.5