Compare commits

...

38 Commits

Author SHA1 Message Date
diced
faa51e794a 2.4.3 2020-11-09 19:13:40 -08:00
diced
23a21cf227 Merge branch 'next' of github.com:diced/zipline into next 2020-11-09 19:13:24 -08:00
diced
a900daa244 2.4.2 2020-11-09 19:12:53 -08:00
diced
42dcd03428 rich content 2020-11-09 19:12:28 -08:00
dicedtomato
b61a283059 Merge pull request #50 from diced/dependabot/npm_and_yarn/find-my-way-3.0.5
Bump find-my-way from 3.0.4 to 3.0.5
2020-11-09 14:37:24 -08:00
dependabot[bot]
801fe8276f Bump find-my-way from 3.0.4 to 3.0.5
Bumps [find-my-way](https://github.com/delvedor/find-my-way) from 3.0.4 to 3.0.5.
- [Release notes](https://github.com/delvedor/find-my-way/releases)
- [Commits](https://github.com/delvedor/find-my-way/compare/v3.0.4...v3.0.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-09 22:34:45 +00:00
dicedtomato
48732be47c Fix #49 2020-11-06 21:57:00 -08:00
diced
353122c169 2.4.1 2020-11-06 20:18:28 -08:00
diced
cea5092fd6 Lighthouse optimizations 2020-11-06 20:18:19 -08:00
diced
3d4625f531 2.4.0 2020-11-06 19:53:34 -08:00
diced
f5ab288bb3 update setup 2020-11-06 19:53:14 -08:00
diced
41565b3a62 move folders to places that make sense 2020-11-06 19:52:54 -08:00
diced
ef7fbaf1dc 2.3.1 2020-11-06 08:42:16 -08:00
diced
87cf861648 remove ioredis 2020-11-06 08:41:59 -08:00
diced
d414c85efd 2.3.0 2020-11-05 19:47:35 -08:00
diced
5b24a8e415 gravatars 2020-11-05 19:47:22 -08:00
diced
1d348db4dd 2.2.4 2020-11-05 19:20:58 -08:00
diced
ececc3ab0e fix workflow 2020-11-05 19:14:33 -08:00
diced
a71bde9730 teyest 2020-11-05 19:12:35 -08:00
diced
3417a84789 remove some useless stuff 2020-11-05 19:10:19 -08:00
diced
080c92a968 2.2.2 2020-11-05 19:08:13 -08:00
diced
94b0220db8 yes 2020-11-05 19:07:47 -08:00
diced
dc29ad42f5 2.2.1 2020-11-05 19:06:25 -08:00
diced
4cce4718a2 2.2.2 2020-11-05 19:06:12 -08:00
diced
8fe1de013f 2.2.1 2020-11-05 19:05:47 -08:00
diced
7a6766e9cc 2.2.2 2020-11-05 19:05:35 -08:00
diced
5537d28849 2.2.1 2020-11-05 19:04:29 -08:00
dicedtomato
a420d830e3 Create codeql-analysis.yml 2020-11-02 21:51:40 -08:00
dicedtomatoreal
43657a8cb0 light theme, if you want to kill your eyes. 2020-11-02 15:24:12 -08:00
dicedtomatoreal
9f6e7717df 2.1.2 2020-11-01 20:03:12 -08:00
dicedtomatoreal
da1bfe5567 check version 2020-11-01 12:00:23 -08:00
dicedtomatoreal
a88fa14d5b fix some errors 2020-11-01 07:53:13 -08:00
dicedtomatoreal
58f934ba30 add comparsion between shares 2020-11-01 07:51:35 -08:00
dicedtomato
bfaca477a8 Merge pull request #48 from mzch/next
fix image file path bug and add deletion of image file entity
2020-10-31 17:29:17 -07:00
dicedtomato
278aa66e00 Update ImagesController.ts 2020-10-31 17:28:28 -07:00
dicedtomatoreal
2c179c8668 add lots of fun badges 2020-10-31 17:23:00 -07:00
Koichi MATSUMOTO
13406a2f67 remove unnecessary word
remove unnecessary word
2020-10-31 14:32:15 +09:00
Koichi MATSUMOTO
cc99001697 fix image file path bug and add deletion of image file entity
fix image file path bug and add deletion of image file entity.
2020-10-31 14:24:29 +09:00
35 changed files with 7898 additions and 9752 deletions

View File

@@ -23,5 +23,6 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm i -g yarn
- run: yarn
- run: yarn build

View File

@@ -1,14 +1,36 @@
## ![](https://raw.githubusercontent.com/ZiplineProject/zipline/next/public/zipline.png)
<p align="center"><img src="https://raw.githubusercontent.com/ZiplineProject/zipline/next/public/zipline_small.png"/></p>
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/29a3d02f0df447acadd721d93229d072)](https://app.codacy.com/gh/ZiplineProject/zipline?utm_source=github.com&utm_medium=referral&utm_content=ZiplineProject/zipline&utm_campaign=Badge_Grade)
![Version](https://img.shields.io/github/package-json/v/dicedtomatoreal/zipline)
![LICENCE](https://img.shields.io/github/license/dicedtomatoreal/zipline)
[![Discord](https://img.shields.io/discord/729771078196527176)](https://discord.gg/AtTSecwqeV)
![Stars](https://img.shields.io/github/stars/dicedtomatoreal/zipline)
![GitHub repo size](https://img.shields.io/github/repo-size/dicedtomatoreal/zipline)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/dicedtomatoreal/zipline/next)
<br>
![David](https://img.shields.io/david/dicedtomatoreal/zipline)
![David](https://img.shields.io/david/dev/dicedtomatoreal/zipline)
![GitHub package.json dependency version (prod)](https://img.shields.io/github/package-json/dependency-version/dicedtomatoreal/zipline/react)
# ZiplineNext
Speed & reliable
# Zipline
The best and only **React + Next.js** ShareX / File Uploader you would ever want.
# Comparison
Wondering how Zipline compares to other popular uploaders? We have done some benchmarking on other popular upload servers, see how Zipline compares.
| Uploader | Average ms (3 batches/1.5k files) |
|-|-|
| **[Zipline](https://github.com/dicedtomatoreal/zipline)** | **61 ms** |
| [ShareX-Upload-Server](https://github.com/TannerReynolds/ShareX-Upload-Server) | 86 ms |
*Note: there were 3 batches of 1.5k requests, the average ms of each was averaged again*<br>
*Note 2: results will vary because its very dependent on the server, location, and your internet (these tests were run on the same machine with local dbs)*
# Features
- Configurable
- Fast (API)
- Built with Next.js & React
- Support for multible database types (mongo soon)
- Support for **multible database types** (*literally the only one that supports multiple dbs*, mongo soon)
# Installing
[See how to install here](https://zipline.diced.wtf/docs/)
# Documentation
You can view current documentation [here](https://zipline.diced.wtf/)

9590
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "zipline-next",
"version": "2.1.0",
"version": "2.4.3",
"private": true,
"dependencies": {
"@dicedtomato/colors": "^1.0.3",
@@ -51,6 +51,7 @@
"@types/node": "^14.11.2",
"@types/react": "^16.9.49",
"@types/react-redux": "^7.1.9",
"@types/semver": "^7.3.4",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"eslint": "^7.10.0",
@@ -60,4 +61,4 @@
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
}
}
}

BIN
public/zipline_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

3
release.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = async (markdown) => {
return markdown;
};

View File

@@ -25,7 +25,7 @@ const base = {
'https://github.githubassets.com/images/modules/open_graph/github-mark.png',
color: '#128377'
},
core: { secret: 'my-secret', port: 3000 },
core: { secret: 'my-secret', port: 3000, host: '127.0.0.1', theme: 'dark', secure: false },
uploader: {
directory: './uploads',
route: '/u',
@@ -48,35 +48,33 @@ const base = {
{ name: 'mysql' },
{ name: 'mariadb' },
{ name: 'mssql' },
{ name: 'sqlite' },
{ name: 'sqlite3' },
{ name: 'mongodb', extra: 'No support yet' }
{ name: 'sqlite3' }
]
},
{
type: 'input',
name: 'host',
message: 'Database Host'
message: 'Database Host (leave blank if sqlite3)'
},
{
type: 'number',
name: 'port',
message: 'Database Port'
message: 'Database Port (leave blank if sqlite3)'
},
{
type: 'input',
name: 'database',
message: 'Database Name'
message: 'Database Name (db path if sqlite3)'
},
{
type: 'input',
name: 'username',
message: 'Database User'
message: 'Database User (leave blank if sqlite3)'
},
{
type: 'password',
name: 'password',
message: 'Database Password'
message: 'Database Password (leave blank if sqlite3)'
}
]);
@@ -106,7 +104,7 @@ const base = {
{
type: 'confirm',
name: 'original',
message: 'Keep Original?'
message: 'Keep Original File names?'
}
]);

View File

@@ -66,29 +66,39 @@ export default function Login() {
</Alert>
</Snackbar>
<Card>
<CardContent>
<Typography variant='h4'>Login</Typography>
<TextField
label='Username'
className={classes.field}
onChange={e => setUsername(e.target.value)}
/>
<TextField
label='Password'
type='password'
className={classes.field}
onChange={e => setPassword(e.target.value)}
/>
</CardContent>
<CardActions>
<Button
color='primary'
className={classes.field}
onClick={handleLogin}
>
Login
</Button>
</CardActions>
<form>
<CardContent>
<Typography variant='h4'>Login</Typography>
<TextField
label='Username'
InputLabelProps={{
htmlFor: 'username'
}}
id='username'
className={classes.field}
onChange={e => setUsername(e.target.value)}
/>
<TextField
label='Password'
type='password'
InputLabelProps={{
htmlFor: 'password'
}}
id='password'
className={classes.field}
onChange={e => setPassword(e.target.value)}
/>
</CardContent>
<CardActions>
<Button
color='primary'
className={classes.field}
onClick={handleLogin}
>
Login
</Button>
</CardActions>
</form>
</Card>
</React.Fragment>
);

View File

@@ -34,13 +34,14 @@ export default function ManageUser() {
const [alertOpen, setAlertOpen] = useState(false);
const [username, setUsername] = useState(state.user.username);
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const handleUpdateUser = async () => {
const d = await (
await fetch('/api/user', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
body: JSON.stringify({ username, password: password.trim() === '' ? null : password, email })
})
).json();
if (!d.error) {
@@ -82,6 +83,14 @@ export default function ManageUser() {
className={classes.field}
onChange={e => setPassword(e.target.value)}
/>
<TextField
label='Email'
type='email'
helperText='Used for a Gravatar Avatar'
value={email}
className={classes.field}
onChange={e => setEmail(e.target.value)}
/>
</CardContent>
<CardActions>
<Button

View File

@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import AppBar from '@material-ui/core/AppBar';
import Box from '@material-ui/core/Box';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Drawer from '@material-ui/core/Drawer';
import Avatar from '@material-ui/core/Avatar';
import Hidden from '@material-ui/core/Hidden';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
@@ -31,11 +33,14 @@ import FileCopyIcon from '@material-ui/icons/FileCopy';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import PublishIcon from '@material-ui/icons/Publish';
import RotateLeftIcon from '@material-ui/icons/RotateLeft';
import Divider from '@material-ui/core/Divider';
import copy from 'copy-to-clipboard';
import { LOGOUT, UPDATE_USER } from '../reducer';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { makeStyles, useTheme, withStyles } from '@material-ui/core/styles';
import { useRouter } from 'next/router';
import { useDispatch } from 'react-redux';
import { store } from '../store';
import { MD5 } from 'crypto-js';
const drawerWidth = 240;
@@ -53,13 +58,13 @@ const useStyles = makeStyles(theme => ({
},
appBar: {
display: 'flex',
backgroundColor: '#000',
color: '#fff',
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#fff',
color: theme.palette.type !== 'dark' ? '#000' : '#fff',
[theme.breakpoints.up('sm')]: {
width: 'calc(100%)',
marginLeft: drawerWidth
},
borderBottom: '1px solid #1f1f1f'
borderBottom: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0'
},
menuButton: {
marginRight: theme.spacing(2),
@@ -84,11 +89,21 @@ const useStyles = makeStyles(theme => ({
}
}));
const NoFocusMenuItem = withStyles(theme => ({
root: {
'&:hover': {
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#f7f7f7'
}
}
}))(MenuItem);
export default function UI({ children }) {
const classes = useStyles();
const theme = useTheme();
const state = store.getState();
const router = useRouter();
const dispatch = useDispatch();
const [emailHash, setEmailHash] = useState('');
const [mobileOpen, setMobileOpen] = useState(false);
const [admin, setAdmin] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
@@ -107,6 +122,8 @@ export default function UI({ children }) {
router.push('/user/login');
} else setAdmin(d.administrator);
})();
setEmailHash(MD5(state.user.email).toString());
}, []);
const handleCopyTokenThenClose = async () => {
@@ -161,16 +178,34 @@ export default function UI({ children }) {
<MenuIcon />
</IconButton>
<Typography variant='h6'>Zipline</Typography>
<IconButton
aria-label='account of current user'
aria-controls='menu-appbar'
aria-haspopup='true'
onClick={event => setAnchorEl(event.currentTarget)}
color='inherit'
className={classes.rightButton}
>
<AccountCircleIcon className={classes.rightButton} />
</IconButton>
<Box className={classes.rightButton}>
{state.user.email ? (
<Button
aria-label='account of current user'
aria-controls='menu-appbar'
aria-haspopup='true'
onClick={event => setAnchorEl(event.currentTarget)}
color='inherit'
className={classes.rightButton}
>
<Avatar src={`https://www.gravatar.com/avatar/${emailHash}.jpg`}>
{state.user.username[0].toUpperCase()}
</Avatar>
</Button>
) : (
<IconButton
aria-label='account of current user'
aria-controls='menu-appbar'
aria-haspopup='true'
onClick={event => setAnchorEl(event.currentTarget)}
color='inherit'
className={classes.rightButton}
>
<AccountCircleIcon className={classes.rightButton} />
</IconButton>
)}
</Box>
<Menu
id='menu-appbar'
anchorEl={anchorEl}
@@ -186,6 +221,12 @@ export default function UI({ children }) {
open={open}
onClose={() => setAnchorEl(null)}
>
<NoFocusMenuItem>
<Typography variant='h6'>
{state.user.username}
</Typography>
</NoFocusMenuItem>
<Divider />
<Link href='/user/manage'>
<MenuItem onClick={() => setAnchorEl(null)}>
<AccountCircleIcon className={classes.menuIcon} />

View File

@@ -0,0 +1,15 @@
import React from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/core/styles';
import dark from '../lib/themes/dark';
import light from '../lib/themes/light';
export default function ZiplineTheming({ Component, pageProps, theme }) {
console.log(theme);
return (
<ThemeProvider theme={theme == 'light' ? light : dark}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
);
}

View File

@@ -12,17 +12,34 @@ import { Console } from './lib/logger';
import { AddressInfo } from 'net';
import { magenta, bold, green, reset, blue, red } from '@dicedtomato/colors';
import { Configuration } from './lib/Config';
import { UserController } from './controllers/UserController';
import { RootController } from './controllers/RootController';
import { UserController } from './lib/api/controllers/UserController';
import { RootController } from './lib/api/controllers/RootController';
import { join } from 'path';
import { ImagesController } from './controllers/ImagesController';
import { URLSController } from './controllers/URLSController';
import { URL } from './entities/URL';
import { ImagesController } from './lib/api/controllers/ImagesController';
import { URLSController } from './lib/api/controllers/URLSController';
import { checkVersion } from './lib/Util';
import { existsSync, readFileSync } from 'fs';
import { Image } from './lib/entities/Image';
import { User } from './lib/entities/User';
import { Zipline } from './lib/entities/Zipline';
import { URL } from './lib/entities/URL';
const dev = process.env.NODE_ENV !== 'production';
(async () => {
if (await checkVersion())
Console.logger('Zipline').info(
'running an outdated version of zipline, please update soon!'
);
})();
console.log(`
${magenta(text('Zipline'))}
Version : ${blue(
process.env.npm_package_version ||
JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
.version
)}
GitHub : ${blue('https://github.com/ZiplineProject/zipline')}
Issues : ${blue('https://github.com/ZiplineProject/zipline/issues')}
Docs : ${blue('https://zipline.diced.wtf/')}
@@ -39,6 +56,9 @@ if (!config) {
process.exit(0);
}
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
const server = fastify({});
const app = next({
dev,
@@ -98,11 +118,38 @@ server.get(`${config.urls.route}/:id`, async function (
return reply.redirect(urlId.url);
});
server.get(`${config.uploader.rich_content_route || '/a'}/:id`, async function (
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
if (!existsSync(join(config.uploader.directory, req.params.id))) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
return reply.type('text/html').send(`
<html>
<head>
<meta property="theme-color" content="${config.meta.color}">
<meta property="og:title" content="${req.params.id}">
<meta property="og:url" content="${config.uploader.route}/${req.params.id}">
<meta property="og:image" content="${config.uploader.route}/${req.params.id}">
<meta property="twitter:card" content="summary_large_image">
</head>
<body>
<div style="text-align:center;vertical-align:middle;">
<img src="${config.uploader.route}/${req.params.id}" >
</div>
</body>
</html>
`);
});
server.register(fastifyMultipart);
server.register(fastifyTypeorm, {
...config.database,
entities: [dev ? './src/entities/**/*.ts' : './dist/entities/**/*.js'],
entities: [Image, URL, User, Zipline],
synchronize: true,
logging: false
});
@@ -121,7 +168,7 @@ server.register(fastifyCookies, {
});
server.register(fastifyStatic, {
root: join(process.cwd(), config.uploader.directory),
root: path,
prefix: config.uploader.route
});
@@ -133,19 +180,24 @@ server.register(fastifyStatic, {
server.register(fastifyFavicon);
server.listen({
port: config.core.port,
host: config.core.host
}, err => {
if (err) throw err;
const info = server.server.address() as AddressInfo;
server.listen(
{
port: config.core.port,
host: config.core.host
},
err => {
if (err) throw err;
const info = server.server.address() as AddressInfo;
Console.logger('Server').info(
`server listening on ${bold(
`${green(info.address)}${reset(':')}${bold(green(info.port.toString()))}`
)}`
);
});
Console.logger('Server').info(
`server listening on ${bold(
`${green(info.address)}${reset(':')}${bold(
green(info.port.toString())
)}`
)}`
);
}
);
server.addHook('preHandler', async (req, reply) => {
if (

View File

@@ -23,6 +23,7 @@ export interface ConfigMeta {
export interface ConfigUploader {
directory: string;
route: string;
rich_content_route?: string;
length: number;
blacklisted: string[];
original: boolean;
@@ -46,6 +47,7 @@ export interface ConfigCore {
secure?: boolean;
blacklisted_ips?: string[];
ratelimiter?: ConfigCoreRateLimiter;
theme?: 'dark' | 'light';
}
export interface ConfigWebhooks {

View File

@@ -2,7 +2,10 @@ import aes from 'crypto-js/aes';
import { compareSync, hashSync } from 'bcrypt';
import { Configuration } from './Config';
import { Connection } from 'typeorm';
import { Zipline } from '../entities/Zipline';
import { compare } from 'semver';
import { Zipline } from './entities/Zipline';
import { readFileSync } from 'fs';
import { join } from 'path';
const config = Configuration.readConfig();
@@ -44,3 +47,15 @@ export async function getFirst(orm: Connection): Promise<boolean> {
if (!d) d = await zipline.save(new Zipline());
return d.first;
}
export async function checkVersion(): Promise<boolean> {
const res = await fetch('https://raw.githubusercontent.com/dicedtomatoreal/zipline/next/package.json');
if (!res.ok) return true;
const latestVersion = (await res.json()).version;
const currentVersion = process.env.npm_package_version || readFileSync(join(process.cwd(), 'package.json'), 'utf8');
const compared = compare(currentVersion, latestVersion);
return compared == 0 || compared == 1 ? false : true;
}

View File

@@ -1,6 +1,6 @@
import { Image } from '../entities/Image';
import { URL } from '../entities/URL';
import { User } from '../entities/User';
import { Image } from './entities/Image';
import { URL } from './entities/URL';
import { User } from './entities/User';
import { Config, Configuration, ConfigWebhooks } from './Config';
import { Console } from './logger';

View File

@@ -1,3 +1,5 @@
import { unlinkSync } from 'fs';
import { join } from 'path';
import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify';
import {
Controller,
@@ -7,12 +9,12 @@ import {
DELETE
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { Image } from '../entities/Image';
import { LoginError } from '../lib/api/APIErrors';
import { Configuration, ConfigWebhooks } from '../lib/Config';
import { Console } from '../lib/logger';
import { readBaseCookie } from '../lib/Util';
import { WebhookHelper, WebhookType } from '../lib/Webhooks';
import { Image } from '../../entities/Image';
import { LoginError } from '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { Console } from '../../logger';
import { readBaseCookie } from '../../Util';
import { WebhookHelper, WebhookType } from '../../Webhooks';
const config = Configuration.readConfig();
@@ -57,14 +59,24 @@ export class ImagesController {
id: req.params.id
});
Console.logger(Image).info(`image ${image.id} was deleted`);
if (this.webhooks.events.includes(WebhookType.DELETE_IMAGE))
WebhookHelper.sendWebhook(this.webhooks.upload.content, {
image,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.uploader.route}/`
});
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = join(dir.charAt(0) == '/' ? dir : join(process.cwd(), dir), image.file);
return reply.send(image);
try {
unlinkSync(path);
Console.logger(Image).info(`image ${image.id} was deleted`);
if (this.webhooks.events.includes(WebhookType.DELETE_IMAGE))
WebhookHelper.sendWebhook(this.webhooks.upload.content, {
image,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.uploader.route}/`
});
return reply.send(image);
} catch (e) {
Console.logger(Image).error(`image ${image.id} could not be deleted...`);
return reply.status(401).send({ error: 'Could not delete image.' });
}
}
@GET('/recent')

View File

@@ -12,18 +12,25 @@ import { join } from 'path';
import { Repository } from 'typeorm';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { Image } from '../entities/Image';
import { User } from '../entities/User';
import { AuthError } from '../lib/api/APIErrors';
import { Configuration, ConfigWebhooks } from '../lib/Config';
import { createRandomId, getFirst } from '../lib/Util';
import { Console } from '../lib/logger';
import { WebhookHelper, WebhookType } from '../lib/Webhooks';
import { Image } from '../../entities/Image';
import { User } from '../../entities/User';
import { AuthError } from '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { createRandomId, getFirst } from '../../Util';
import { Console } from '../../logger';
import { WebhookHelper, WebhookType } from '../../Webhooks';
const pump = promisify(pipeline);
const config = Configuration.readConfig();
const rateLimiterConfig = config.core.ratelimiter
? { config: { rateLimit: { max: config.core.ratelimiter.requests, timeWindow: config.core.ratelimiter.retry_after } } }
? {
config: {
rateLimit: {
max: config.core.ratelimiter.requests,
timeWindow: config.core.ratelimiter.retry_after
}
}
}
: {};
@Controller('/api')
@@ -42,6 +49,11 @@ export class RootController {
return first;
}
@GET('/theme')
async getTheme() {
return { theme: config.core.theme || 'dark' };
}
@GET('/users')
async allUsers(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new Error('Not logged in.');
@@ -130,14 +142,18 @@ export class RootController {
`image ${fileName}.${ext} was uploaded by ${user.username} (${user.id})`
);
const host = `${config.core.secure ? 'https' : 'http'}://${req.hostname}${
config.uploader.rich_content_route
? config.uploader.rich_content_route
: config.uploader.route
}/`;
if (this.webhooks.events.includes(WebhookType.UPLOAD))
WebhookHelper.sendWebhook(this.webhooks.upload.content, {
image,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.uploader.route}/`
host
});
reply.send(
`${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.uploader.route}/${fileName}.${ext}`
);
reply.send(host);
}
}

View File

@@ -8,13 +8,13 @@ import {
POST
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { URL } from '../entities/URL';
import { User } from '../entities/User';
import { LoginError } from '../lib/api/APIErrors';
import { Configuration, ConfigWebhooks } from '../lib/Config';
import { Console } from '../lib/logger';
import { createRandomId, readBaseCookie } from '../lib/Util';
import { WebhookType, WebhookHelper } from '../lib/Webhooks';
import { URL } from '../../entities/URL';
import { User } from '../../entities/User';
import { LoginError } from '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { Console } from '../../logger';
import { createRandomId, readBaseCookie } from '../../Util';
import { WebhookType, WebhookHelper } from '../../Webhooks';
const config = Configuration.readConfig();

View File

@@ -9,16 +9,16 @@ import {
DELETE
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { User } from '../entities/User';
import { Zipline } from '../entities/Zipline';
import { User } from '../../entities/User';
import { Zipline } from '../../entities/Zipline';
import {
UserNotFoundError,
MissingBodyData,
LoginError,
UserExistsError
} from '../lib/api/APIErrors';
import { Configuration, ConfigWebhooks } from '../lib/Config';
import { Console } from '../lib/logger';
} from '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { Console } from '../../logger';
import {
checkPassword,
createBaseCookie,
@@ -26,8 +26,8 @@ import {
encryptPassword,
getFirst,
readBaseCookie
} from '../lib/Util';
import { WebhookType, WebhookHelper } from '../lib/Webhooks';
} from '../../Util';
import { WebhookType, WebhookHelper } from '../../Webhooks';
const config = Configuration.readConfig();
@@ -63,7 +63,7 @@ export class UserController {
@PATCH('/')
async editUser(
req: FastifyRequest<{ Body: { username: string; password: string } }>,
req: FastifyRequest<{ Body: { username: string; password: string, email: string; } }>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
@@ -78,7 +78,8 @@ export class UserController {
this.logger.verbose(`attempting to save ${user.username} (${user.id})`);
user.username = req.body.username;
user.password = encryptPassword(req.body.password);
if (req.body.password) user.password = encryptPassword(req.body.password);
if (req.body.email) user.email = req.body.email;
await this.users.save(user);
this.logger.info(`saved ${user.username} (${user.id})`);
@@ -236,9 +237,7 @@ export class UserController {
this.logger.verbose(
`attempting to delete ${existing.username} (${existing.id})`
);
await this.users.delete({
id: existing.id
});
await this.users.remove(existing);
this.logger.info(`deleted ${existing.username} (${existing.id})`);
if (this.webhooks.events.includes(WebhookType.USER_DELETE))

View File

@@ -11,6 +11,9 @@ export class User {
@Column('text')
public password: string;
@Column('text', { default: null }) /* used for gravatar avatar! */
public email: string;
@Column('boolean', { default: false })
public administrator: boolean;
@@ -25,6 +28,7 @@ export class User {
) {
this.username = username;
this.password = password;
this.email = null;
this.administrator = administrator;
this.token = token;
}

View File

@@ -9,6 +9,7 @@ declare global {
}
}
if (!global.logr) global.logr = { formatter: null };
export enum ConsoleLevel {

38
src/lib/themes/light.ts Normal file
View File

@@ -0,0 +1,38 @@
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
const lightTheme = createMuiTheme({
palette: {
type: 'light',
primary: {
main: '#000000'
},
secondary: {
main: '#4a5bb0'
},
background: {
default: '#fff',
paper: '#f7f7f7'
}
},
overrides: {
MuiListItem: {
root: {
'&$selected': {
backgroundColor: '#e0e0e0'
}
}
},
MuiCard: {
root: {
backgroundColor: '#fff'
}
},
MuiButton: {
root: {
margin: '132'
}
}
}
});
export default lightTheme;

View File

@@ -1,21 +1,26 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/core/styles';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '../store';
import theme from '../lib/themes/dark';
import ZiplineTheming from '../components/ZiplineTheming';
function MyApp({ Component, pageProps }) {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
}, []);
(async () => {
const d = await (await fetch('/api/theme')).json();
if (!d.error) setTheme(d.theme);
})();
}, []);
return (
<ThemeProvider theme={theme}>
<React.Fragment>
<Head>
<title>Zipline</title>
<meta
@@ -25,12 +30,11 @@ function MyApp({ Component, pageProps }) {
</Head>
<Provider store={store}>
<PersistGate loading={<div>loading</div>} persistor={persistor}>
<CssBaseline />
<Component {...pageProps} />
<PersistGate loading={<div>Loading...</div>} persistor={persistor}>
<ZiplineTheming Component={Component} pageProps={pageProps} theme={theme} />
</PersistGate>
</Provider>
</ThemeProvider>
</React.Fragment>
);
}
@@ -39,4 +43,4 @@ MyApp.propTypes = {
pageProps: PropTypes.object.isRequired
};
export default MyApp;
export default MyApp;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
@@ -33,7 +34,7 @@ export default class MyDocument extends Document<DocumentProps> {
</Head>
) : null}
<body>
<Main />
<Main/>
<NextScript />
</body>
</Html>
@@ -43,17 +44,18 @@ export default class MyDocument extends Document<DocumentProps> {
MyDocument.getInitialProps = async ctx => {
const sheets = new ServerStyleSheets();
const config = Configuration.readConfig();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheets.collect(<App {...props} />)
enhanceApp: App => props => sheets.collect(<App {...props}/>)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
config: Configuration.readConfig(),
config,
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement()

View File

@@ -19,7 +19,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {

View File

@@ -16,7 +16,7 @@ import UI from '../../components/UI';
import UIPlaceholder from '../../components/UIPlaceholder';
import { makeStyles } from '@material-ui/core';
import { store } from '../../store';
import { Image } from '../../entities/Image';
import { Image } from '../../lib/entities/Image';
import { Configuration } from '../../lib/Config';
const useStyles = makeStyles(theme => ({
@@ -24,7 +24,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {
@@ -136,18 +136,18 @@ export default function Images({ config }) {
<Pagination count={chunks.length} onChange={changePage} />
</>
) : (
<Grid
container
spacing={0}
direction='column'
alignItems='center'
justify='center'
>
<Grid item xs={6} sm={12}>
<AddToPhotosIcon style={{ fontSize: 100 }} />
</Grid>
<Grid
container
spacing={0}
direction='column'
alignItems='center'
justify='center'
>
<Grid item xs={6} sm={12}>
<AddToPhotosIcon style={{ fontSize: 100 }} />
</Grid>
)}
</Grid>
)}
</Paper>
) : null}
<Popover

View File

@@ -20,7 +20,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {
@@ -28,7 +28,7 @@ const useStyles = makeStyles(theme => ({
color: '#fff'
},
tableBorder: {
borderColor: '#121212'
borderColor: theme.palette.type === 'dark' ? '#1f1f1f' : '#e0e0e0'
}
}));

View File

@@ -17,7 +17,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {

View File

@@ -24,7 +24,7 @@ import copy from 'copy-to-clipboard';
import UI from '../../components/UI';
import UIPlaceholder from '../../components/UIPlaceholder';
import { makeStyles } from '@material-ui/core';
import { URL as URLEntity } from '../../entities/URL';
import { URL as URLEntity } from '../../lib/entities/URL';
import { Configuration } from '../../lib/Config';
const useStyles = makeStyles(theme => ({
@@ -32,7 +32,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {

View File

@@ -30,7 +30,7 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: '1px solid #1f1f1f',
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
field: {
@@ -219,7 +219,7 @@ export default function Users() {
}
title={`${u.username} (${u.id})`}
subheader={`${u.administrator ? 'Administrator' : 'User'
}`}
}`}
/>
</Card>
</Grid>

View File

@@ -1,11 +1,12 @@
/* eslint-disable indent */
import { User } from './entities/User';
import { User } from './lib/entities/User';
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export const UPDATE_USER = 'UPDATE_USER';
export const STOP_LOADING = 'STOP_LOADING';
export const START_LOADING = 'START_LOADING';
export const SET_THEME = 'SET_THEME';
export interface State {
loggedIn: boolean;

7490
yarn.lock Normal file

File diff suppressed because it is too large Load Diff