Compare commits

..

21 Commits
2.3.0 ... 2.5.2

Author SHA1 Message Date
diced
e3fe4e4254 2.5.2 2020-11-11 11:47:37 -08:00
diced
6dee11b4dc optimization 2020-11-11 11:47:26 -08:00
diced
7126fd67c6 2.5.1 2020-11-10 20:12:04 -08:00
diced
1c22ccfa20 add features 2020-11-10 20:11:56 -08:00
diced
ef12842a5e add new config opts 2020-11-10 20:11:51 -08:00
diced
1089dcfc73 2.5.0 2020-11-10 20:01:54 -08:00
diced
bbae2776f6 MultiFactor Auth 2020-11-10 20:01:31 -08:00
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
26 changed files with 659 additions and 355 deletions

View File

@@ -30,6 +30,9 @@ Wondering how Zipline compares to other popular uploaders? We have done some ben
- Fast (API)
- Built with Next.js & React
- Support for **multible database types** (*literally the only one that supports multiple dbs*, mongo soon)
- Token protected uploading
- MFA with Authy/Google Authenticator
- Easy setup instructions on [docs](https://zipline.diced.wtf/docs)
# Installing
[See how to install here](https://zipline.diced.wtf/docs/)

View File

@@ -1,6 +1,6 @@
{
"name": "zipline-next",
"version": "2.3.0",
"version": "2.5.2",
"private": true,
"dependencies": {
"@dicedtomato/colors": "^1.0.3",
@@ -23,16 +23,17 @@
"fastify-typeorm-plugin": "^2.1.2",
"figlet": "^1.5.0",
"inquirer": "^7.3.3",
"ioredis": "^4.19.2",
"material-ui-dropzone": "^3.5.0",
"next": "^9.5.4",
"pg": "^8.4.1",
"qrcode": "^1.4.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-helmet": "^6.1.0",
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"speakeasy": "^2.0.0",
"toml-patch": "^0.2.3",
"typeorm": "^0.2.28"
},
@@ -48,9 +49,9 @@
"devDependencies": {
"@types/bcrypt": "^3.0.0",
"@types/crypto-js": "^3.1.47",
"@types/ioredis": "^4.17.7",
"@types/mongodb": "^3.5.27",
"@types/node": "^14.11.2",
"@types/qrcode": "^1.3.5",
"@types/react": "^16.9.49",
"@types/react-redux": "^7.1.9",
"@types/semver": "^7.3.4",

View File

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

View File

@@ -4,7 +4,7 @@ const { stringify } = require('toml-patch');
const { writeFileSync } = require('fs');
const { join } = require('path');
const createDockerCompose = (port) => {
const createDockerCompose = port => {
return `version: "3"
services:
zipline:
@@ -25,7 +25,14 @@ const base = {
'https://github.githubassets.com/images/modules/open_graph/github-mark.png',
color: '#128377'
},
core: { secret: 'my-secret', port: 3000, host: '127.0.0.1', theme: 'dark', secure: false },
core: {
secret: 'my-secret',
port: 3000,
host: '127.0.0.1',
theme: 'dark',
secure: false,
mfa: false
},
uploader: {
directory: './uploads',
route: '/u',
@@ -33,7 +40,7 @@ const base = {
original: false,
blacklisted: []
},
urls: { route: '/s', length: 4, vanity: false }
urls: { route: '/go', length: 4, vanity: true }
};
(async () => {
@@ -48,35 +55,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)'
}
]);
@@ -92,6 +97,20 @@ const base = {
type: 'number',
name: 'port',
message: 'Serve on Port'
},
{
type: 'list',
name: 'theme',
message: 'Theme',
choices: [
{ name: 'Dark Theme (recomended)' },
{ name: 'Light Theme (warning for eyes)' }
]
},
{
type: 'confirm',
name: 'mfa',
message: 'Enable MFA with Authy/Google Authenticator'
}
]);
@@ -106,7 +125,7 @@ const base = {
{
type: 'confirm',
name: 'original',
message: 'Keep Original?'
message: 'Keep Original File names?'
}
]);
@@ -138,10 +157,14 @@ const base = {
urls: { ...base.urls, ...urls }
};
writeFileSync('Zipline.toml', stringify(config));
if (docker.useDocker) {
config.core.host = '0.0.0.0';
console.log('Generating docker-compose.yml...');
writeFileSync('docker-compose.yml', createDockerCompose(config.core.port));
console.log(
'Head to https://zipline.diced.wtf/docs/docker to learn how to run with docker.'
);
}
writeFileSync('Zipline.toml', stringify(config));
})();

View File

@@ -6,6 +6,10 @@ import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Snackbar from '@material-ui/core/Snackbar';
import Alert from '@material-ui/lab/Alert';
import makeStyles from '@material-ui/core/styles/makeStyles';
@@ -26,8 +30,11 @@ export default function Login() {
const classes = useStyles();
const router = useRouter();
const dispatch = useDispatch();
const [error, setError] = useState(false);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [authOpen, setAuthOpen] = useState(false);
const [token, setToken] = useState('');
const [open, setOpen] = useState(false);
const handleClose = (event, reason) => {
@@ -37,17 +44,48 @@ export default function Login() {
const handleLogin = async () => {
const d = await (
await fetch('/api/user/login', {
await fetch('/api/user/verify-login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
).json();
if (!d.error) {
dispatch({ type: UPDATE_USER, payload: d });
if (d.mfa) {
setAuthOpen(true);
} else {
const payload = await (
await fetch('/api/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
).json();
if (!payload.error) {
dispatch({ type: UPDATE_USER, payload });
dispatch({ type: LOGIN });
router.push('/dash');
}
}
} else setOpen(true);
};
const tryChecking = async () => {
const verified = await (
await fetch(`/api/mfa/verify?token=${token}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
).json();
if (!verified.passed) setError(true);
else {
setError(false);
setAuthOpen(false);
dispatch({ type: UPDATE_USER, payload: verified.user });
dispatch({ type: LOGIN });
router.push('/dash');
} else setOpen(true);
}
};
return (
@@ -65,30 +103,63 @@ export default function Login() {
Could not login!
</Alert>
</Snackbar>
<Card>
<CardContent>
<Typography variant='h4'>Login</Typography>
<Dialog
open={authOpen}
onClose={() => setAuthOpen(false)}
aria-labelledby='enable-mfa'
aria-describedby='mfa-desc'
>
<DialogTitle id='enable-mfa'>2FA</DialogTitle>
<DialogContent>
<TextField
label='Username'
label='Code'
helperText={error ? 'Incorrect code' : ''}
value={token}
className={classes.field}
onChange={e => setUsername(e.target.value)}
onChange={e => setToken(e.target.value)}
error={error}
/>
<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
</DialogContent>
<DialogActions>
<Button color='primary' autoFocus onClick={tryChecking}>
Check
</Button>
</CardActions>
</DialogActions>
</Dialog>
<Card>
<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

@@ -3,14 +3,21 @@ import Typography from '@material-ui/core/Typography';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Snackbar from '@material-ui/core/Snackbar';
import Grid from '@material-ui/core/Grid';
import Alert from '@material-ui/lab/Alert';
import { makeStyles } from '@material-ui/core';
import { UPDATE_USER } from '../reducer';
import { store } from '../store';
import { useDispatch } from 'react-redux';
import { Config } from '../lib/Config';
const useStyles = makeStyles({
margin: {
@@ -27,28 +34,70 @@ const useStyles = makeStyles({
}
});
export default function ManageUser() {
export default function ManageUser({ config }: { config: Config }) {
const classes = useStyles();
const dispatch = useDispatch();
const state = store.getState();
const [alertOpen, setAlertOpen] = useState(false);
const [mfaDialogOpen, setMfaDialogOpen] = useState(false);
const [qrcode, setQRCode] = useState(null);
const [username, setUsername] = useState(state.user.username);
const [password, setPassword] = useState('');
const [mfaToken, setMfaToken] = useState('');
const [email, setEmail] = useState('');
const [error, setError] = useState(false);
const handleUpdateUser = async () => {
const d = await (
await fetch('/api/user', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password: password.trim() === '' ? null : password, email })
body: JSON.stringify({
username,
password: password.trim() === '' ? null : password,
email
})
})
).json();
if (!d.error) {
dispatch({ type: UPDATE_USER, payload: d });
setAlertOpen(true);
}
};
const disableMFA = async () => {
await fetch('/api/mfa/disable');
const d = await (await fetch('/api/user')).json();
if (!d.error) {
dispatch({ type: UPDATE_USER, payload: d });
setAlertOpen(true);
}
};
const enableMFA = async () => {
setMfaDialogOpen(true);
const { dataURL } = await (await fetch('/api/mfa/qrcode')).json();
setQRCode(dataURL);
};
const tryEnablingMfa = async () => {
const verified = await (
await fetch(`/api/mfa/verify?token=${mfaToken}`)
).json();
if (!verified) setError(true);
else {
setError(false);
setMfaDialogOpen(false);
setAlertOpen(true);
const d = await (await fetch('/api/user')).json();
if (!d.error) {
dispatch({ type: UPDATE_USER, payload: d });
setAlertOpen(true);
}
}
};
return (
<React.Fragment>
<Snackbar
@@ -64,6 +113,41 @@ export default function ManageUser() {
Updated <b>{state.user.username}</b>
</Alert>
</Snackbar>
<Dialog
open={mfaDialogOpen}
onClose={() => setMfaDialogOpen(false)}
aria-labelledby='enable-mfa'
aria-describedby='mfa-desc'
>
<DialogTitle id='enable-mfa'>Enable 2FA</DialogTitle>
<DialogContent>
<DialogContentText id='mfa-desc'>
When enabling 2FA/MFA you can use <b>Authy</b> or{' '}
<b>Google Authenticator</b> to authenticate before logging into
Zipline.
</DialogContentText>
<Grid container spacing={2}>
<Grid item xs={6}>
<img src={qrcode} />
</Grid>
<Grid item xs={6}>
<TextField
label='Code'
helperText='After scanning the QRCode, enter the authentication code here'
value={mfaToken}
className={classes.field}
onChange={e => setMfaToken(e.target.value)}
error={error}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button color='primary' autoFocus onClick={tryEnablingMfa}>
Enable
</Button>
</DialogActions>
</Dialog>
<Card>
<CardContent>
<Typography color='textSecondary' variant='h4' gutterBottom>
@@ -100,6 +184,15 @@ export default function ManageUser() {
>
Update
</Button>
{config.core.mfa ? (
<Button
className={classes.button}
color='primary'
onClick={state.user.secretMfaKey ? disableMFA : enableMFA}
>
{state.user.secretMfaKey ? 'Disable MFA' : 'Enable MFA'}
</Button>
) : null}
</CardActions>
</Card>
</React.Fragment>

View File

@@ -1,175 +1,21 @@
import React from 'react';
import AppBar from '@material-ui/core/AppBar';
import Drawer from '@material-ui/core/Drawer';
import Hidden from '@material-ui/core/Hidden';
import IconButton from '@material-ui/core/IconButton';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Backdrop from '@material-ui/core/Backdrop';
import CircularProgress from '@material-ui/core/CircularProgress';
import Grid from '@material-ui/core/Grid';
import MenuIcon from '@material-ui/icons/Menu';
import Skeleton from '@material-ui/lab/Skeleton';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex'
},
drawer: {
[theme.breakpoints.up('sm')]: {
width: 240,
flexShrink: 0
}
},
appBar: {
[theme.breakpoints.up('sm')]: {
width: 'calc(100%)',
marginLeft: 240
}
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
display: 'none'
}
},
rightButton: {
marginLeft: 'auto'
},
// necessary for content to be below app bar
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: 240
},
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
fullWidth: {
width: '100%'
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#000'
}
}));
export default function UIPlaceholder() {
const classes = useStyles();
const theme = useTheme();
const [mobileOpen, setMobileOpen] = React.useState(false);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const drawer = (
<div>
<Toolbar>
<AppBar position='fixed' className={classes.appBar} elevation={0}>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<Typography variant='h6' noWrap>
Zipline
</Typography>
<div className={classes.rightButton}>
<Skeleton animation='wave' className={classes.fullWidth} />
</div>
</Toolbar>
</AppBar>
</Toolbar>
<List>
<ListItem button key='Home'>
<Skeleton animation='wave' className={classes.fullWidth} />
</ListItem>
<ListItem button key='Statistics'>
<Skeleton animation='wave' className={classes.fullWidth} />
</ListItem>
<ListItem button key='Images'>
<Skeleton animation='wave' className={classes.fullWidth} />
</ListItem>
<ListItem button key='URLs'>
<Skeleton animation='wave' className={classes.fullWidth} />
</ListItem>
</List>
</div>
);
const container =
typeof window !== 'undefined' ? () => window.document.body : undefined;
return (
<div className={classes.root}>
<AppBar position='fixed' className={classes.appBar} elevation={0}>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<Typography variant='h6' noWrap>
Zipline
</Typography>
</Toolbar>
</AppBar>
<nav className={classes.drawer} aria-label='mailbox folders'>
<Hidden smUp implementation='css'>
<Drawer
container={container}
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={mobileOpen}
onClose={handleDrawerToggle}
classes={{
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true // Better open performance on mobile.
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden xsDown implementation='css'>
<Drawer
classes={{
paper: classes.drawerPaper
}}
variant='permanent'
open
PaperProps={{ style: { border: 'none' } }}
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
<Grid
container
spacing={0}
direction='column'
alignItems='center'
justify='center'
style={{ minHeight: '80vh' }}
>
<Grid item xs={3}>
<CircularProgress size={100} />
</Grid>
</Grid>
</main>
</div>
<Backdrop className={classes.backdrop} open={true}>
<CircularProgress color='inherit' />
</Backdrop>
);
}

View File

@@ -11,27 +11,36 @@ import { bootstrap } from 'fastify-decorators';
import { Console } from './lib/logger';
import { AddressInfo } from 'net';
import { magenta, bold, green, reset, blue, red } from '@dicedtomato/colors';
import Redis from 'ioredis';
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 { ImagesController } from './lib/api/controllers/ImagesController';
import { URLSController } from './lib/api/controllers/URLSController';
import { checkVersion } from './lib/Util';
import { readFileSync } from 'fs';
import { Image } from './entities/Image';
import { User } from './entities/User';
import { Zipline } from './entities/Zipline';
import { URL } from './entities/URL';
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';
import { MultiFactorController } from './lib/api/controllers/MultiFactorController';
const dev = process.env.NODE_ENV !== 'production';
(async () => { if (await checkVersion()) Console.logger('Zipline').info('running an outdated version of zipline, please update soon!'); })();
(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 || readFileSync(join(process.cwd(), 'package.json'), 'utf8'))}
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/')}
@@ -110,6 +119,33 @@ 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, {
@@ -124,7 +160,8 @@ server.register(bootstrap, {
UserController,
RootController,
ImagesController,
URLSController
URLSController,
MultiFactorController
]
});
@@ -145,22 +182,26 @@ 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 (
config.core.blacklisted_ips &&
config.core.blacklisted_ips.includes(req.ip)
@@ -168,4 +209,4 @@ server.addHook('preHandler', async (req, reply) => {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
});
});

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;
@@ -47,6 +48,7 @@ export interface ConfigCore {
blacklisted_ips?: string[];
ratelimiter?: ConfigCoreRateLimiter;
theme?: 'dark' | 'light';
mfa?: boolean;
}
export interface ConfigWebhooks {

View File

@@ -3,7 +3,7 @@ import { compareSync, hashSync } from 'bcrypt';
import { Configuration } from './Config';
import { Connection } from 'typeorm';
import { compare } from 'semver';
import { Zipline } from '../entities/Zipline';
import { Zipline } from './entities/Zipline';
import { readFileSync } from 'fs';
import { join } from 'path';

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

@@ -9,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();

View File

@@ -0,0 +1,127 @@
import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify';
import {
Controller,
GET,
FastifyInstanceToken,
Inject,
POST
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { User } from '../../entities/User';
import { totp, generateSecret, otpauthURL } from 'speakeasy';
import { toDataURL } from 'qrcode';
import { checkPassword, createBaseCookie, readBaseCookie } from '../../Util';
import { LoginError, MissingBodyData, UserNotFoundError } from '../APIErrors';
import { Console } from '../../logger';
import { WebhookType, WebhookHelper } from '../../Webhooks';
import { Configuration, ConfigWebhooks } from '../../Config';
const config = Configuration.readConfig();
@Controller('/api/mfa')
export class MultiFactorController {
@Inject(FastifyInstanceToken)
private instance!: FastifyInstance;
private users: Repository<User> = this.instance.orm.getRepository(User);
private logger: Console = Console.logger(User);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
@GET('/qrcode')
async qrcode(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
let user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
}
});
if (!user.secretMfaKey) {
const secret = generateSecret({
issuer: 'Zipline',
length: 128,
name: user.username
});
user.secretMfaKey = secret.base32;
user = await this.users.save(user);
}
const dataURL = await toDataURL(
otpauthURL({
secret: user.secretMfaKey,
label: user.email || 'none',
issuer: 'Zipline',
encoding: 'base32'
})
);
return reply.send({
dataURL
});
}
@GET('/disable')
async disable(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
const user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
}
});
user.secretMfaKey = null;
this.users.save(user);
reply.send({ disabled: true });
}
@POST('/verify')
async verify(
req: FastifyRequest<{
Querystring: { token: string };
Body: { username: string; password: string };
}>,
reply: FastifyReply
) {
if (req.cookies.zipline) throw new LoginError('Already logged in.');
if (!req.body.username) throw new MissingBodyData('Missing username.');
if (!req.body.password) throw new MissingBodyData('Missing uassword.');
const user = await this.users.findOne({
where: {
username: req.body.username
}
});
if (!user)
throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
if (!checkPassword(req.body.password, user.password)) {
this.logger.error(
`${user.username} (${user.id}) tried to login but failed`
);
throw new LoginError('Wrong credentials!');
}
delete user.password;
const passed = totp.verify({
encoding: 'base32',
token: req.query.token,
secret: user.secretMfaKey
});
this.logger.verbose(`set cookie for ${user.username} (${user.id})`);
reply.setCookie('zipline', createBaseCookie(user.id), {
path: '/',
maxAge: 1036800000
});
this.logger.info(`${user.username} (${user.id}) logged in`);
if (this.webhooks.events.includes(WebhookType.LOGIN))
WebhookHelper.sendWebhook(this.webhooks.login.content, {
user
});
return reply.send({ user, passed });
}
}

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')
@@ -135,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,9 @@ export class UserController {
@PATCH('/')
async editUser(
req: FastifyRequest<{ Body: { username: string; password: string, email: string; } }>,
req: FastifyRequest<{
Body: { username: string; password: string; email: string };
}>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
@@ -92,6 +94,35 @@ export class UserController {
return reply.send(user);
}
@POST('/verify-login')
async verify(
req: FastifyRequest<{ Body: { username: string; password: string } }>,
reply: FastifyReply
) {
if (req.cookies.zipline) throw new LoginError('Already logged in.');
if (!req.body.username) throw new MissingBodyData('Missing username.');
if (!req.body.password) throw new MissingBodyData('Missing uassword.');
const user = await this.users.findOne({
where: {
username: req.body.username
}
});
if (!user)
throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
if (!checkPassword(req.body.password, user.password)) {
this.logger.error(
`${user.username} (${user.id}) tried to verify their credentials but failed`
);
throw new LoginError('Wrong credentials!');
}
reply.send({
mfa: !!user.secretMfaKey
});
}
@POST('/login')
async login(
req: FastifyRequest<{ Body: { username: string; password: string } }>,
@@ -250,7 +281,6 @@ export class UserController {
throw new Error(`Could not delete user: ${e.message}`);
}
}
// @Hook('preValidation')
// public async preValidation(req: FastifyRequest, reply: FastifyReply) {
// // const adminRoutes = ['/api/user/create'];

View File

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

View File

@@ -5,7 +5,7 @@ import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '../store';
import ZiplineTheming from '../components/ZiplineTheming';
import UIPlaceholder from '../components/UIPlaceholder';
function MyApp({ Component, pageProps }) {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
@@ -19,7 +19,6 @@ function MyApp({ Component, pageProps }) {
})();
}, []);
return (
<React.Fragment>
<Head>
<title>Zipline</title>
@@ -30,8 +29,12 @@ function MyApp({ Component, pageProps }) {
</Head>
<Provider store={store}>
<PersistGate loading={<div>Loading...</div>} persistor={persistor}>
<ZiplineTheming Component={Component} pageProps={pageProps} theme={theme} />
<PersistGate loading={<UIPlaceholder />} persistor={persistor}>
<ZiplineTheming
Component={Component}
pageProps={pageProps}
theme={theme}
/>
</PersistGate>
</Provider>
</React.Fragment>
@@ -43,4 +46,4 @@ MyApp.propTypes = {
pageProps: PropTypes.object.isRequired
};
export default MyApp;
export default MyApp;

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 => ({

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 => ({

View File

@@ -4,18 +4,25 @@ import UI from '../../components/UI';
import UIPlaceholder from '../../components/UIPlaceholder';
import ManageUser from '../../components/ManageUser';
import { store } from '../../store';
import { Configuration } from '../../lib/Config';
export default function Manage() {
export default function Manage({ config }) {
const router = useRouter();
const state = store.getState();
if (typeof window !== 'undefined' && !state.loggedIn) router.push('/user/login');
if (typeof window !== 'undefined' && !state.loggedIn)
router.push('/user/login');
else {
return (
<UI>
<ManageUser />
<ManageUser config={config} />
</UI>
);
}
return <UIPlaceholder />;
}
export async function getStaticProps() {
const config = Configuration.readConfig();
return { props: { config } };
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable indent */
import { User } from './entities/User';
import { User } from './lib/entities/User';
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';

186
yarn.lock
View File

@@ -1293,13 +1293,6 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/ioredis@^4.17.7":
version "4.17.7"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.17.7.tgz#5007a56380fd387315efabc6c777c050feabb121"
integrity sha512-M8/KDSGmNYhPFMn+CxDDpIEHP27b2muEHgnK1UgIQIMEO2KXH9mznHx3epeRlD+AjF94HFh3dy2/9zqO+pPDqQ==
dependencies:
"@types/node" "*"
"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
@@ -1323,6 +1316,13 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/qrcode@^1.3.5":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.5.tgz#9c97cc2875f03e2b16a0d89856fc48414e380c38"
integrity sha512-92QMnMb9m0ErBU20za5Eqtf4lzUcSkk5w/Cz30q5qod0lWHm2loztmFs2EnCY06yT51GY1+m/oFq2D8qVK2Bjg==
dependencies:
"@types/node" "*"
"@types/react-redux@^7.1.9":
version "7.1.11"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.11.tgz#a18e8ab3651e8e8cc94798934927937c66021217"
@@ -1924,6 +1924,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
base32.js@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
integrity sha1-0EVzalex9sE58MffQlGKhOkbsro=
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
@@ -2117,6 +2122,24 @@ bson@^1.1.4:
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.5.tgz#2aaae98fcdf6750c0848b0cba1ddec3c73060a34"
integrity sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
dependencies:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
buffer-from@^1.0.0, buffer-from@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2149,7 +2172,7 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^5.5.0:
buffer@^5.4.3, buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -2413,6 +2436,15 @@ cli-width@^3.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
cliui@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
dependencies:
string-width "^3.1.0"
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
@@ -2436,11 +2468,6 @@ clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@@ -2852,7 +2879,7 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
denque@^1.1.0, denque@^1.4.1:
denque@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
@@ -2901,6 +2928,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dijkstrajs@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -3651,9 +3683,9 @@ find-cache-dir@^2.1.0:
pkg-dir "^3.0.0"
find-my-way@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.4.tgz#a485973d1a3fdafd989ac9f12fd2d88e83cda268"
integrity sha512-Trl/mNAVvTgCpo9ox6yixkwiZUvecKYUQZoAuMCBACsgGqv+FbWe+jE5sBA5+U8LIWrJk/cw8zPV53GPrjTnsw==
version "3.0.5"
resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-3.0.5.tgz#f71c5ef1b4865401e1b97ba428121a8f55439eec"
integrity sha512-FweGg0cv1sBX8z7WhvBX5B5AECW4Zdh/NiB38Oa0qwSNIyPgRBCl/YjxuZn/rz38E/MMBHeVKJ22i7W3c626Gg==
dependencies:
fast-decode-uri-component "^1.0.1"
safe-regex2 "^2.0.0"
@@ -4161,22 +4193,6 @@ invariant@^2.2.2:
dependencies:
loose-envify "^1.0.0"
ioredis@^4.19.2:
version "4.19.2"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.19.2.tgz#e3eab394c653cea5aea07c0c784d8c772dce8801"
integrity sha512-SZSIwMrbd96b7rJvJwyTWSP6XQ0m1kAIIqBnwglJKrIJ6na7TeY4F2EV2vDY0xm/fLrUY8cEg81dR7kVFt2sKA==
dependencies:
cluster-key-slot "^1.1.0"
debug "^4.1.1"
denque "^1.1.0"
lodash.defaults "^4.2.0"
lodash.flatten "^4.4.0"
p-map "^2.1.0"
redis-commands "1.6.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.0.1"
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
@@ -4375,6 +4391,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4637,16 +4658,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@@ -5390,11 +5401,6 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-map@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
@@ -5619,6 +5625,11 @@ platform@1.3.3:
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461"
integrity sha1-ZGx3ARiZhwtqCQPnXpl+jlHadGE=
pngjs@^3.3.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
pnp-webpack-plugin@1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@@ -5844,6 +5855,19 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qrcode@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
dependencies:
buffer "^5.4.3"
buffer-alloc "^1.2.0"
buffer-from "^1.1.1"
dijkstrajs "^1.0.1"
isarray "^2.0.1"
pngjs "^3.3.0"
yargs "^13.2.4"
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6011,23 +6035,6 @@ readdirp@~3.5.0:
dependencies:
picomatch "^2.2.1"
redis-commands@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23"
integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==
redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=
dependencies:
redis-errors "^1.0.0"
redux-persist@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
@@ -6598,6 +6605,13 @@ sparse-bitfield@^3.0.3:
dependencies:
memory-pager "^1.0.2"
speakeasy@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
integrity sha1-hckaBxsJpcuGQlkNmDVmFl9XYTo=
dependencies:
base32.js "0.0.1"
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -6638,11 +6652,6 @@ stacktrace-parser@0.1.10:
dependencies:
type-fest "^0.7.1"
standard-as-callback@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126"
integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -6733,7 +6742,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^3.0.0:
string-width@^3.0.0, string-width@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -6814,7 +6823,7 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
strip-ansi@^5.1.0:
strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -7429,6 +7438,15 @@ worker-farm@^1.7.0:
dependencies:
errno "~0.1.7"
wrap-ansi@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
dependencies:
ansi-styles "^3.2.0"
string-width "^3.0.0"
strip-ansi "^5.0.0"
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -7506,6 +7524,14 @@ yargonaut@^1.1.2:
figlet "^1.1.1"
parent-require "^1.0.0"
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
@@ -7519,6 +7545,22 @@ yargs-parser@^20.2.2:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26"
integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==
yargs@^13.2.4:
version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
dependencies:
cliui "^5.0.0"
find-up "^3.0.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^3.0.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.2"
yargs@^15.0.0:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"