mirror of
https://github.com/diced/zipline.git
synced 2025-12-25 20:34:31 -08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4728258750 | ||
|
|
ece3e16459 | ||
|
|
9208dbe2f3 | ||
|
|
636de18642 | ||
|
|
ee48456291 | ||
|
|
a06d5ffaed | ||
|
|
606821a2c0 | ||
|
|
5c980c21e5 | ||
|
|
771cc380df | ||
|
|
38217870fe | ||
|
|
5b82c96a43 | ||
|
|
6f5f9869ad | ||
|
|
b29bfeb8b1 | ||
|
|
cb40559e49 | ||
|
|
90c72f7ffe | ||
|
|
002bd2e6f7 | ||
|
|
7b44f17a64 |
@@ -1,3 +1,5 @@
|
||||
prisma/migrations
|
||||
prisma
|
||||
node_modules
|
||||
.next
|
||||
.next
|
||||
uploads
|
||||
.git
|
||||
|
||||
37
README.md
37
README.md
@@ -1,25 +1,28 @@
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/></p>
|
||||
|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||

|
||||

|
||||

|
||||
<br>
|
||||
|
||||
# Zipline
|
||||
|
||||
Fast & lightweight file uploading.
|
||||
|
||||
# Features
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||
Zipline is a file sharing, URL sharing, lightweight and easy to use!
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://discord.gg/EAhCRfGxCF)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
|
||||
|
||||
# Installing
|
||||
## Installing
|
||||
[See how to install here](https://zipline.diced.me/get-started)
|
||||
|
||||
[See how to install here](https://zipline.diced.me/docs/getting-started)
|
||||
## Configuration
|
||||
[See how to configure here](https://zipline.diced.me/configuration/overview)
|
||||
|
||||
## Theming
|
||||
[See how to theme here](https://zipline.diced.me/themes)
|
||||
|
||||
@@ -47,7 +47,8 @@ module.exports = {
|
||||
'middleware',
|
||||
'redux',
|
||||
'themes',
|
||||
'lib'
|
||||
'lib',
|
||||
'assets'
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zip3",
|
||||
"version": "3.2.1",
|
||||
"version": "3.2.2",
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"dev": "NODE_ENV=development node server",
|
||||
@@ -17,10 +17,9 @@
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@material-ui/core": "^5.0.0-alpha.37",
|
||||
"@material-ui/data-grid": "^4.0.0-alpha.32",
|
||||
"@material-ui/icons": "^5.0.0-alpha.37",
|
||||
"@material-ui/styles": "^5.0.0-alpha.35",
|
||||
"@prisma/client": "^2.30.3",
|
||||
"@prisma/client": "^3.0.2",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"colorette": "^1.2.2",
|
||||
@@ -30,7 +29,7 @@
|
||||
"formik": "^2.2.9",
|
||||
"multer": "^1.4.2",
|
||||
"next": "11.1.1",
|
||||
"prisma": "^2.30.3",
|
||||
"prisma": "^3.0.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -12,9 +12,9 @@ const prismaRun = require('../scripts/prisma-run');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('../scripts/mimes');
|
||||
const deployDb = require('../scripts/deploy-db');
|
||||
const { version } = require('../package.json');
|
||||
|
||||
|
||||
Logger.get('server').info('starting zipline server');
|
||||
Logger.get('server').info(`starting zipline@${version} server`);
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
@@ -42,10 +42,11 @@ function shouldUseYarn() {
|
||||
Logger.get('database').info('some migrations are not applied, applying them now...');
|
||||
await deployDb(config);
|
||||
Logger.get('database').info('finished applying migrations');
|
||||
} else {
|
||||
Logger.get('database').info('migrations up to date');
|
||||
}
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
|
||||
await stat('./.next');
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
|
||||
const app = next({
|
||||
@@ -55,12 +56,13 @@ function shouldUseYarn() {
|
||||
}, config.core.port, config.core.host);
|
||||
|
||||
await app.prepare();
|
||||
await stat('./.next');
|
||||
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const srv = createServer(async (req, res) => {
|
||||
if (req.url.startsWith('/raw')) {
|
||||
if (req.url.startsWith('/r')) {
|
||||
const parts = req.url.split('/');
|
||||
if (!parts[2] || parts[2] === '') return;
|
||||
|
||||
@@ -121,6 +123,7 @@ function shouldUseYarn() {
|
||||
});
|
||||
srv.on('listening', () => {
|
||||
Logger.get('server').info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
if (process.platform === 'linux' && dev) execSync(`xdg-open ${config.core.secure ? 'https' : 'http'}://${config.core.host === '0.0.0.0' ? 'localhost' : config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function Card(props) {
|
||||
const { name, children, ...other } = props;
|
||||
|
||||
return (
|
||||
<MuiCard sx={{ minWidth: 100 }} {...other}>
|
||||
<MuiCard sx={{ minWidth: '100%' }} {...other}>
|
||||
<CardContent>
|
||||
<Typography variant='h3'>{name}</Typography>
|
||||
{children}
|
||||
|
||||
@@ -4,64 +4,80 @@ import {
|
||||
Card,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
Popover,
|
||||
Button,
|
||||
ButtonGroup
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent
|
||||
} from '@material-ui/core';
|
||||
import AudioIcon from '@material-ui/icons/Audiotrack';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import useFetch from '../lib/hooks/useFetch';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
export default function Image({ image, updateImages }) {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [t,] = useState(image.mimetype.split('/')[0]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
if (!res.error) updateImages(true);
|
||||
|
||||
setAnchorEl(null);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setAnchorEl(null);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
const data = await useFetch('/api/user/images', 'PATCH', { id: image.id, favorite: !image.favorite });
|
||||
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
|
||||
if (!data.error) updateImages(true);
|
||||
};
|
||||
|
||||
const Type = (props) => {
|
||||
return {
|
||||
'video': <video controls {...props} />,
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
'image': <img {...props} />,
|
||||
'audio': <audio controls {...props} />
|
||||
}[t];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ maxWidth: '100%' }}>
|
||||
<CardActionArea>
|
||||
<CardActionArea sx={t === 'audio' ? { justifyContent: 'center', display: 'flex', alignItems: 'center' } : {}}>
|
||||
<CardMedia
|
||||
sx={{ height: 320 }}
|
||||
sx={{ height: 320, fontSize: 70, width: '100%' }}
|
||||
image={image.url}
|
||||
title={image.file}
|
||||
onClick={e => setAnchorEl(e.currentTarget)}
|
||||
component={t === 'audio' ? AudioIcon : t} // this is done because audio without controls is hidden
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<Popover
|
||||
open={Boolean(anchorEl)}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<ButtonGroup variant='contained'>
|
||||
<Button onClick={handleDelete} color='primary'>Delete</Button>
|
||||
<Button onClick={handleCopy} color='primary'>Copy URL</Button>
|
||||
<Button onClick={handleFavorite} color='primary'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
</ButtonGroup>
|
||||
</Popover>
|
||||
<DialogTitle id='alert-dialog-title'>
|
||||
{image.file}
|
||||
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Type
|
||||
style={{ width: '100%' }}
|
||||
src={image.url}
|
||||
alt={image.url}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDelete} color='inherit'>Delete</Button>
|
||||
<Button onClick={handleCopy} color='inherit'>Copy URL</Button>
|
||||
<Button onClick={handleFavorite} color='inherit'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import useFetch from '../lib/hooks/useFetch';
|
||||
|
||||
import {
|
||||
AppBar,
|
||||
@@ -29,13 +28,13 @@ import {
|
||||
Menu as MenuIcon,
|
||||
Home as HomeIcon,
|
||||
AccountCircle as AccountIcon,
|
||||
Image as ImageIcon,
|
||||
Folder as FolderIcon,
|
||||
Upload as UploadIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
Autorenew as ResetIcon,
|
||||
Logout as LogoutIcon,
|
||||
PeopleAlt as UsersIcon,
|
||||
Brush as BrushIcon
|
||||
Brush as BrushIcon,
|
||||
} from '@material-ui/icons';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import Backdrop from './Backdrop';
|
||||
@@ -43,6 +42,7 @@ import { friendlyThemeName, themes } from './Theming';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -51,9 +51,9 @@ const items = [
|
||||
link: '/dashboard'
|
||||
},
|
||||
{
|
||||
icon: <ImageIcon />,
|
||||
text: 'Images',
|
||||
link: '/dashboard/images'
|
||||
icon: <FolderIcon />,
|
||||
text: 'Files',
|
||||
link: '/dashboard/files'
|
||||
},
|
||||
{
|
||||
icon: <UploadIcon />,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { CssBaseline } from '@material-ui/core';
|
||||
|
||||
// themes
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import dark from 'lib/themes/dark';
|
||||
import ayu_dark from 'lib/themes/ayu_dark';
|
||||
@@ -9,6 +11,7 @@ import ayu_light from 'lib/themes/ayu_light';
|
||||
import nord from 'lib/themes/nord';
|
||||
import polar from 'lib/themes/polar';
|
||||
import dracula from 'lib/themes/dracula';
|
||||
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import createTheme from 'lib/themes';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -11,12 +10,16 @@ import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Typography,
|
||||
Grid
|
||||
Grid,
|
||||
Skeleton,
|
||||
CardActionArea,
|
||||
CardMedia,
|
||||
Card as MuiCard
|
||||
} from '@material-ui/core';
|
||||
import AudioIcon from '@material-ui/icons/Audiotrack';
|
||||
|
||||
import Link from 'components/Link';
|
||||
import Card from 'components/Card';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
|
||||
@@ -59,18 +62,19 @@ function StatTable({ rows, columns }) {
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map(col => (
|
||||
<TableCell key={col.name}>{col.name}</TableCell>
|
||||
<TableCell key={col.name} sx={{ borderColor: t => t.palette.divider }}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => (
|
||||
{rows.map(row => (
|
||||
<TableRow
|
||||
hover
|
||||
key={row.username}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
{columns.map(col => (
|
||||
<TableCell key={col.id}>
|
||||
<TableCell key={col.id} sx={{ borderColor: t => t.palette.divider }}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</TableCell>
|
||||
))}
|
||||
@@ -86,33 +90,31 @@ export default function Dashboard() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
const [images, setImages] = useState([]);
|
||||
const [recent, setRecent] = useState([]);
|
||||
const [page, setPage] = useState(0);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [apiLoading, setApiLoading] = useState(true);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
const updateImages = async () => {
|
||||
setApiLoading(true);
|
||||
|
||||
const imgs = await useFetch('/api/user/images');
|
||||
const imgs = await useFetch('/api/user/files');
|
||||
const recent = await useFetch('/api/user/recent?filter=media');
|
||||
const stts = await useFetch('/api/stats');
|
||||
setImages(imgs);
|
||||
setStats(stts);
|
||||
|
||||
setApiLoading(false);
|
||||
setRecent(recent);
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
const handleChangeRowsPerPage = event => {
|
||||
setRowsPerPage(+event.target.value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleDelete = async image => {
|
||||
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
|
||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||
if (!res.error) updateImages();
|
||||
};
|
||||
|
||||
@@ -122,70 +124,88 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop open={apiLoading} />
|
||||
<Typography variant='h4'>Welcome back {user?.username}</Typography>
|
||||
<Typography color='GrayText' pb={2}>You have <b>{images.length}</b> images</Typography>
|
||||
|
||||
<Typography color='GrayText' pb={2}>You have <b>{images.length ? images.length : '...'}</b> images</Typography>
|
||||
|
||||
<Typography variant='h4'>Recent Images</Typography>
|
||||
<Grid container spacing={4} py={2}>
|
||||
{recent.length ? recent.map(image => (
|
||||
<Grid item xs={12} sm={3} key={image.id}>
|
||||
<MuiCard sx={{ minWidth: '100%' }}>
|
||||
<CardActionArea>
|
||||
<CardMedia
|
||||
sx={{ height: 220 }}
|
||||
image={image.url}
|
||||
title={image.file}
|
||||
controls
|
||||
component={image.mimetype.split('/')[0] === 'audio' ? AudioIcon : image.mimetype.split('/')[0]} // this is done because audio without controls is hidden
|
||||
/>
|
||||
</CardActionArea>
|
||||
</MuiCard>
|
||||
</Grid>
|
||||
)) : [1,2,3,4].map(x => (
|
||||
<Grid item xs={12} sm={3} key={x}>
|
||||
<Skeleton variant='rectangular' width='100%' height={220} sx={{ borderRadius: 1 }}/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
<Typography variant='h4'>Stats</Typography>
|
||||
{stats && (
|
||||
<Grid container spacing={4} py={2}>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Size' sx={{ height: '100%' }}>
|
||||
<StatText>{stats.size}</StatText>
|
||||
<Typography variant='h3'>Average Size</Typography>
|
||||
<StatText>{bytesToRead(stats.size_num / stats.count)}</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Images' sx={{ height: '100%' }}>
|
||||
<StatText>{stats.count}</StatText>
|
||||
<Typography variant='h3'>Views</Typography>
|
||||
<StatText>{stats.views_count} ({isNaN(stats.views_count / stats.count) ? '0' : stats.views_count / stats.count})</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Users' sx={{ height: '100%' }}>
|
||||
<StatText>{stats.count_users}</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid container spacing={4} py={2}>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Size' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.size : <Skeleton variant='text' />}</StatText>
|
||||
<Typography variant='h3'>Average Size</Typography>
|
||||
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton variant='text' />}</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Images' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count : <Skeleton variant='text' />}</StatText>
|
||||
<Typography variant='h3'>Views</Typography>
|
||||
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? '0' : stats.views_count / stats.count})` : <Skeleton variant='text' />}</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Card name='Users' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count_users : <Skeleton variant='text' />}</StatText>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Card name='Images' sx={{ my: 2 }} elevation={0} variant='outlined'>
|
||||
<Link href='/dashboard/images' pb={2}>View Gallery</Link>
|
||||
<TableContainer sx={{ maxHeight: 440 }}>
|
||||
<Table size='small'>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
{columns.map(column => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
sx={{ minWidth: column.minWidth }}
|
||||
sx={{ minWidth: column.minWidth, borderColor: t => t.palette.divider }}
|
||||
>
|
||||
{column.label}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell sx={{ minWidth: 200 }} align='right'>
|
||||
Actions
|
||||
<TableCell sx={{ minWidth: 200, borderColor: t => t.palette.divider }} align='right'>
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{images
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row) => {
|
||||
.map(row => {
|
||||
return (
|
||||
<TableRow hover role='checkbox' tabIndex={-1} key={row.id}>
|
||||
{columns.map((column) => {
|
||||
{columns.map(column => {
|
||||
const value = row[column.id];
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
<TableCell key={column.id} align={column.align} sx={{ borderColor: t => t.palette.divider }}>
|
||||
{column.format ? column.format(value) : value}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
<TableCell align='right'>
|
||||
<TableCell align='right' sx={{ borderColor: t => t.palette.divider }}>
|
||||
<ButtonGroup variant='outlined'>
|
||||
<Button onClick={() => handleDelete(row)} color='error' size='small'>Delete</Button>
|
||||
</ButtonGroup>
|
||||
@@ -203,32 +223,24 @@ export default function Dashboard() {
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
onRowsPerPageChange={handleChangeRowsPerPage} />
|
||||
</Card>
|
||||
<Card name='Images per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
{ id: 'count', name: 'Images' }
|
||||
]}
|
||||
rows={stats ? stats.count_by_user : []} />
|
||||
</Card>
|
||||
<Card name='Types' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'mimetype', name: 'Type' },
|
||||
{ id: 'count', name: 'Count' }
|
||||
]}
|
||||
rows={stats ? stats.types_count : []} />
|
||||
</Card>
|
||||
{stats && (
|
||||
<>
|
||||
<Card name='Images per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
{ id: 'count', name: 'Images' }
|
||||
]}
|
||||
rows={stats.count_by_user}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card name='Types' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'mimetype', name: 'Type' },
|
||||
{ id: 'count', name: 'Count' }
|
||||
]}
|
||||
rows={stats.types_count}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Grid, Pagination, Box, Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core';
|
||||
import { ExpandMore } from '@material-ui/icons';
|
||||
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import ZiplineImage from 'components/Image';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { ExpandMore } from '@material-ui/icons';
|
||||
|
||||
export default function Upload() {
|
||||
export default function Files() {
|
||||
const [pages, setPages] = useState([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [favoritePages, setFavoritePages] = useState([]);
|
||||
@@ -15,9 +15,9 @@ export default function Upload() {
|
||||
|
||||
const updatePages = async favorite => {
|
||||
setLoading(true);
|
||||
const pages = await useFetch('/api/user/images?paged=true&filter=image');
|
||||
const pages = await useFetch('/api/user/files?paged=true&filter=media');
|
||||
if (favorite) {
|
||||
const fPages = await useFetch('/api/user/images?paged=true&favorite=true');
|
||||
const fPages = await useFetch('/api/user/files?paged=true&favorite=media');
|
||||
setFavoritePages(fPages);
|
||||
}
|
||||
setPages(pages);
|
||||
@@ -39,13 +39,13 @@ export default function Upload() {
|
||||
pt={2}
|
||||
pb={3}
|
||||
>
|
||||
<Typography variant='h4'>No Images</Typography>
|
||||
<Typography variant='h4'>No Files</Typography>
|
||||
</Box>
|
||||
) : <Typography variant='h4'>Images</Typography>}
|
||||
) : <Typography variant='h4'>Files</Typography>}
|
||||
{favoritePages.length ? (
|
||||
<Accordion sx={{ my: 2, border: 1, borderColor: t => t.palette.divider }} elevation={0}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Typography variant='h4'>Favorite Images</Typography>
|
||||
<Typography variant='h4'>Favorite Files</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
@@ -167,7 +167,6 @@ export default function Manage() {
|
||||
validationSchema: themeValidationSchema,
|
||||
onSubmit: async values => {
|
||||
setLoading(true);
|
||||
|
||||
const newUser = await useFetch('/api/user', 'PATCH', { customTheme: values });
|
||||
|
||||
if (newUser.error) {
|
||||
|
||||
@@ -8,19 +8,21 @@ import Alert from 'components/Alert';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import CenteredBox from 'components/CenteredBox';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import Link from 'components/Link';
|
||||
|
||||
export default function Upload({ route }) {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
const [file, setFile] = useState(null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [severity, setSeverity] = useState('success');
|
||||
const [message, setMessage] = useState('Saved');
|
||||
|
||||
|
||||
const handleUpload = async () => {
|
||||
const body = new FormData();
|
||||
body.append('file', file);
|
||||
|
||||
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
|
||||
|
||||
setLoading(true);
|
||||
const res = await fetch('/api/upload', {
|
||||
@@ -34,8 +36,11 @@ export default function Upload({ route }) {
|
||||
if (res.ok && json.error === undefined) {
|
||||
setOpen(true);
|
||||
setSeverity('success');
|
||||
setMessage(`Copied to clipboard! ${json.url}`);
|
||||
|
||||
//@ts-ignore
|
||||
setMessage(<>Copied first image to clipboard! <br/>{json.files.map(x => (<Link key={x} href={x}>{x}<br/></Link>))}</>);
|
||||
copy(json.url);
|
||||
setFiles([]);
|
||||
} else {
|
||||
setOpen(true);
|
||||
setSeverity('error');
|
||||
@@ -50,7 +55,7 @@ export default function Upload({ route }) {
|
||||
<Alert open={open} setOpen={setOpen} message={message} severity={severity} />
|
||||
|
||||
<Typography variant='h4' pb={2}>Upload file</Typography>
|
||||
<Dropzone onDrop={acceptedFiles => setFile(acceptedFiles[0])}>
|
||||
<Dropzone onDrop={acceptedFiles => setFiles([...files, ...acceptedFiles])}>
|
||||
{({getRootProps, getInputProps}) => (
|
||||
<CardActionArea>
|
||||
<Paper
|
||||
@@ -67,7 +72,9 @@ export default function Upload({ route }) {
|
||||
<input {...getInputProps()} />
|
||||
<CenteredBox><UploadIcon sx={{ fontSize: 100 }} /></CenteredBox>
|
||||
<CenteredBox><Typography variant='h5'>Drag an image or click to upload an image.</Typography></CenteredBox>
|
||||
<CenteredBox><Typography variant='h6'>{file && file.name}</Typography></CenteredBox>
|
||||
{files.map(file => (
|
||||
<CenteredBox key={file.name}><Typography variant='h6'>{file.name}</Typography></CenteredBox>
|
||||
))}
|
||||
</Paper>
|
||||
</CardActionArea>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,7 @@ export type NextApiReq = NextApiRequest & {
|
||||
} | null | void>;
|
||||
getCookie: (name: string) => string | null;
|
||||
cleanCookie: (name: string) => void;
|
||||
file?: NextApiFile;
|
||||
files?: NextApiFile[];
|
||||
}
|
||||
|
||||
export type NextApiRes = NextApiResponse & {
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function createTheme(o: ThemeOptions) {
|
||||
backgroundColor: o.border
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -19,9 +19,6 @@ export interface ConfigUploader {
|
||||
// The route uploads will be served on
|
||||
route: string;
|
||||
|
||||
// The route embedded routes will be served on
|
||||
embed_route: string;
|
||||
|
||||
// Length of random chars to generate for file names
|
||||
length: number;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GetServerSideProps } from 'next';
|
||||
import { Box } from '@material-ui/core';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import getFile from '../../server/static';
|
||||
|
||||
export default function EmbeddedImage({ image, title, username, color, normal, embed }) {
|
||||
const dataURL = (route: string) => `${route}/${image.file}`;
|
||||
@@ -12,19 +13,13 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
|
||||
const imageEl = document.getElementById('image_content') as HTMLImageElement;
|
||||
|
||||
const original = new Image;
|
||||
original.src = dataURL('/raw');
|
||||
original.src = dataURL('/r');
|
||||
|
||||
if (original.width > innerWidth) {
|
||||
imageEl.width = Math.floor(original.width * Math.min((innerHeight / original.height), (innerWidth / original.width)));
|
||||
imageEl.height = innerHeight;
|
||||
} else {
|
||||
imageEl.width = original.width;
|
||||
imageEl.height = original.height;
|
||||
}
|
||||
if (original.width > innerWidth) imageEl.width = Math.floor(original.width * Math.min((innerHeight / original.height), (innerWidth / original.width)));
|
||||
else imageEl.width = original.width;
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') window.onresize = () => updateImage();
|
||||
|
||||
useEffect(() => updateImage(), []);
|
||||
|
||||
return (
|
||||
@@ -44,7 +39,7 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
|
||||
<meta property='og:url' content={dataURL(normal)} />
|
||||
</>
|
||||
)}
|
||||
<meta property='og:image' content={dataURL('/raw')} />
|
||||
<meta property='og:image' content={dataURL('/r')} />
|
||||
<meta property='twitter:card' content='summary_large_image' />
|
||||
<title>{image.file}</title>
|
||||
</Head>
|
||||
@@ -54,7 +49,7 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
|
||||
alignItems='center'
|
||||
minHeight='100vh'
|
||||
>
|
||||
<img src={dataURL('/raw')} alt={dataURL('/raw')} id='image_content' />
|
||||
<img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
@@ -86,6 +81,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
|
||||
if (!image) return { notFound: true };
|
||||
|
||||
if (!image.embed) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
embedTitle: true,
|
||||
@@ -97,11 +100,12 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (!image.mimetype.startsWith('image')) return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: `raw/${image.file}`,
|
||||
}
|
||||
if (!image.mimetype.startsWith('image')) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import { checkPassword, createToken, hashPassword } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
import prismaRun from '../../../../scripts/prisma-run';
|
||||
import config from 'lib/config';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method !== 'POST') return res.status(405).end();
|
||||
@@ -11,7 +9,16 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
const users = await prisma.user.findMany();
|
||||
if (users.length === 0) {
|
||||
await prismaRun(config.core.database_url, ['db', 'seed', '--preview-feature']);
|
||||
Logger.get('database').info('no users found... creating default user...');
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
username: 'administrator',
|
||||
password: await hashPassword('password'),
|
||||
token: createToken(),
|
||||
administrator: true
|
||||
}
|
||||
});
|
||||
Logger.get('database').info('created default user:\nUsername: "administrator"\nPassword: "password"');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
@@ -25,7 +32,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const valid = await checkPassword(password, user.password);
|
||||
if (!valid) return res.forbid('Wrong password');
|
||||
|
||||
res.setCookie('user', user.id, { sameSite: true, maxAge: 10000000, path: '/' });
|
||||
// 604800 seconds is 1 week
|
||||
res.setCookie('user', user.id, { sameSite: true, maxAge: 604800, path: '/' });
|
||||
|
||||
Logger.get('user').info(`User ${user.username} (${user.id}) logged in`);
|
||||
|
||||
|
||||
@@ -43,21 +43,19 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
by: ['mimetype'],
|
||||
_count: {
|
||||
mimetype: true
|
||||
}
|
||||
},
|
||||
});
|
||||
const types_count = [];
|
||||
for (let i = 0, L = typesCount.length; i !== L; ++i) {
|
||||
types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
|
||||
}
|
||||
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
|
||||
|
||||
return res.json({
|
||||
size: bytesToRead(size),
|
||||
size_num: size,
|
||||
count,
|
||||
count_by_user,
|
||||
count_by_user: count_by_user.sort((a,b) => b.count-a.count),
|
||||
count_users,
|
||||
views_count: (viewsCount[0]?._sum?.views ?? 0),
|
||||
types_count
|
||||
types_count: types_count.sort((a,b) => b.count-a.count)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,33 +20,41 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
token: req.headers.authorization
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!user) return res.forbid('authorization incorect');
|
||||
if (!req.file) return res.error('no file');
|
||||
if (req.file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error('file size too big');
|
||||
if (!req.files) return res.error('no files');
|
||||
if (req.files && req.files.length === 0) return res.error('no files');
|
||||
|
||||
const ext = req.file.originalname.split('.').pop();
|
||||
if (zconfig.uploader.disabled_extentions.includes(ext)) return res.error('disabled extension recieved: ' + ext);
|
||||
const rand = randomChars(zconfig.uploader.length);
|
||||
const files = [];
|
||||
|
||||
let invis;
|
||||
const image = await prisma.image.create({
|
||||
data: {
|
||||
file: `${rand}.${ext}`,
|
||||
mimetype: req.file.mimetype,
|
||||
userId: user.id,
|
||||
embed: !!req.headers.embed
|
||||
}
|
||||
});
|
||||
|
||||
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
|
||||
for (let i = 0; i !== req.files.length; ++i) {
|
||||
const file = req.files[i];
|
||||
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error('file size too big');
|
||||
|
||||
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), req.file.buffer);
|
||||
const ext = file.originalname.split('.').pop();
|
||||
if (zconfig.uploader.disabled_extentions.includes(ext)) return res.error('disabled extension recieved: ' + ext);
|
||||
const rand = randomChars(zconfig.uploader.length);
|
||||
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
let invis;
|
||||
const image = await prisma.image.create({
|
||||
data: {
|
||||
file: `${rand}.${ext}`,
|
||||
mimetype: file.mimetype,
|
||||
userId: user.id,
|
||||
embed: !!req.headers.embed
|
||||
}
|
||||
});
|
||||
|
||||
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
|
||||
|
||||
return res.json({
|
||||
url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`
|
||||
});
|
||||
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), file.buffer);
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
files.push(`${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
}
|
||||
|
||||
// url will be deprecated soon
|
||||
return res.json({ files, url: files[0] });
|
||||
}
|
||||
|
||||
function run(middleware: any) {
|
||||
@@ -60,7 +68,7 @@ function run(middleware: any) {
|
||||
}
|
||||
|
||||
export default async function handlers(req, res) {
|
||||
await run(uploader.single('file'))(req, res);
|
||||
await run(uploader.array('file'))(req, res);
|
||||
|
||||
return withZipline(handler)(req, res);
|
||||
};
|
||||
|
||||
@@ -54,8 +54,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
images.map(image => image.url = `/raw/${image.file}`);
|
||||
if (req.query.filter && req.query.filter === 'image') images = images.filter(x => x.mimetype.startsWith('image'));
|
||||
images.map(image => image.url = `/r/${image.file}`);
|
||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
|
||||
|
||||
return res.json(req.query.paged ? chunk(images, 16) : images);
|
||||
}
|
||||
@@ -16,10 +16,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.username) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { username: req.body.username }
|
||||
});
|
||||
if (req.body.username) {
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: req.body.username
|
||||
}
|
||||
});
|
||||
if (existing && user.username !== req.body.username) {
|
||||
return res.forbid('Username is already taken');
|
||||
}
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { username: req.body.username }
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.embedTitle) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
@@ -82,4 +92,4 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
}
|
||||
}
|
||||
|
||||
export default withZipline(handler);
|
||||
export default withZipline(handler);
|
||||
|
||||
@@ -5,11 +5,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
if (!user) return res.forbid('not logged in');
|
||||
|
||||
const take = Number(req.query.take ?? 3);
|
||||
const take = Number(req.query.take ?? 4);
|
||||
|
||||
if (take > 50) return res.error('take can\'t be more than 50');
|
||||
|
||||
const images = await prisma.image.findMany({
|
||||
let images = await prisma.image.findMany({
|
||||
take,
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
@@ -21,6 +21,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
images.map(image => image.url = `/r/${image.file}`);
|
||||
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
|
||||
|
||||
return res.json(images);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Images from 'components/pages/Images';
|
||||
import Files from 'components/pages/Files';
|
||||
|
||||
export default function ImagesPage() {
|
||||
const { user, loading } = useLogin();
|
||||
@@ -14,7 +14,7 @@ export default function ImagesPage() {
|
||||
loading={loading}
|
||||
noPaper={false}
|
||||
>
|
||||
<Images />
|
||||
<Files />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
48
yarn.lock
48
yarn.lock
@@ -399,16 +399,6 @@
|
||||
react-is "^17.0.0"
|
||||
react-transition-group "^4.4.0"
|
||||
|
||||
"@material-ui/data-grid@^4.0.0-alpha.32":
|
||||
version "4.0.0-alpha.32"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/data-grid/-/data-grid-4.0.0-alpha.32.tgz#72952d1dea9a9440a02827dd66996a8e94d3f28f"
|
||||
integrity sha512-yEmQ8OGGHCB9fUx6f6/ncIxAQpH/U/295EqqocCsbIjLJA1rUYF5eseo2/jnW1Wd69o3aTsXozdlKV8tQNit2Q==
|
||||
dependencies:
|
||||
"@material-ui/utils" "^5.0.0-alpha.14"
|
||||
clsx "^1.0.4"
|
||||
prop-types "^15.7.2"
|
||||
reselect "^4.0.0"
|
||||
|
||||
"@material-ui/icons@^5.0.0-alpha.37":
|
||||
version "5.0.0-alpha.37"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-5.0.0-alpha.37.tgz#8579986e0a3b4586dc7fbf23234d8bfe909b6c3c"
|
||||
@@ -487,7 +477,7 @@
|
||||
prop-types "^15.7.2"
|
||||
react-is "^17.0.0"
|
||||
|
||||
"@material-ui/utils@5.0.0-alpha.35", "@material-ui/utils@^5.0.0-alpha.14":
|
||||
"@material-ui/utils@5.0.0-alpha.35":
|
||||
version "5.0.0-alpha.35"
|
||||
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-alpha.35.tgz#89078592c42bca5db712e82e12d56cd4be737c01"
|
||||
integrity sha512-Msu+zIXd7Y2JrTU9JIf0xjjjAMdWEIdlj2Tmj9bSYFF6bgStrQ1WXXZxxFz5GmdzT7FcLi5U3PqBynSNX/QDGA==
|
||||
@@ -612,22 +602,22 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||
|
||||
"@prisma/client@^2.30.3":
|
||||
version "2.30.3"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.3.tgz#49c1015e2cec26a44b20c62eb2fd738cb0bb043b"
|
||||
integrity sha512-Ey2miZ+Hne12We3rA8XrlPoAF0iuKEhw5IK2nropaelSt0Ju3b2qSz9Qt50a/1Mx3+7yRSu/iSXt8y9TUMl/Yw==
|
||||
"@prisma/client@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1"
|
||||
integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
|
||||
"@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
|
||||
"@prisma/engines-version@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20":
|
||||
version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#d5ef55c92beeba56e52bba12b703af0bfd30530d"
|
||||
integrity sha512-/iDRgaoSQC77WN2oDsOM8dn61fykm6tnZUAClY+6p+XJbOEgZ9gy4CKuKTBgrjSGDVjtQ/S2KGcYd3Ring8xaw==
|
||||
"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
|
||||
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779"
|
||||
integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ==
|
||||
|
||||
"@prisma/engines@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20":
|
||||
version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#2df768aa7c9f84acaa1f35c970417822233a9fb1"
|
||||
integrity sha512-WPnA/IUrxDihrRhdP6+8KAVSwsc0zsh8ioPYsLJjOhzVhwpRbuFH2tJDRIAbc+qFh+BbTIZbeyBYt8fpNXaYQQ==
|
||||
"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
|
||||
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621"
|
||||
integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg==
|
||||
|
||||
"@reduxjs/toolkit@^1.6.0":
|
||||
version "1.6.0"
|
||||
@@ -4444,12 +4434,12 @@ prepend-http@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prisma@^2.30.3:
|
||||
version "2.30.3"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.30.3.tgz#e4a770e1f52151e72c1c5be0aa2e75222a0135c4"
|
||||
integrity sha512-48qYba2BIyUmXuosBZs0g3kYGrxKvo4VkSHYOuLlDdDirmKyvoY2hCYMUYHSx3f++8ovfgs+MX5KmNlP+iAZrQ==
|
||||
prisma@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.2.tgz#e86cb6abf4a815c7ac97b9d0ed383f01c253ce34"
|
||||
integrity sha512-TyOCbtWGDVdWvsM1RhUzJXoGClXGalHhyYWIc5eizSF8T1ScGiOa34asBUdTnXOUBFSErbsqMNw40DHAteBm1A==
|
||||
dependencies:
|
||||
"@prisma/engines" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
|
||||
"@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
|
||||
Reference in New Issue
Block a user