Compare commits

..

60 Commits
2.5.2 ... 2.9.0

Author SHA1 Message Date
diced
1ed55cae21 2.9.0 2021-04-05 15:48:37 -07:00
diced
c3027b9369 b 2021-04-05 15:47:27 -07:00
diced
819fd1a2b1 THEMES 2021-04-05 15:36:48 -07:00
diced
b5d64b0798 lol 2021-04-05 13:28:44 -07:00
diced
dc01caeab0 lol 2021-04-05 13:27:57 -07:00
dicedtomato
df646f7433 Update docker-hub.yml 2021-03-26 16:55:23 -07:00
dicedtomato
4239b610fc Create docker-hub.yml 2021-03-26 16:54:00 -07:00
diced
44640b7d26 yes 2021-03-26 15:07:08 -07:00
diced
1745298f47 maybe fix mariadb 2021-03-26 15:04:06 -07:00
diced
0bd83f731f windows scripts #65 2021-03-26 14:59:45 -07:00
dicedtomato
749ca44ac5 Merge pull request #64 from diced/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4
2021-03-10 22:24:53 -08:00
dependabot[bot]
2857bae6be Bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-11 06:07:29 +00:00
diced
a72d1e1d66 2.8.1 2021-02-12 10:03:33 -08:00
diced
f45e06ef9e Char limit 2021-02-12 10:03:25 -08:00
diced
3bf154c6d0 Sign cookies & fix users 2021-02-12 10:02:01 -08:00
diced
bc9fa4e063 Merge branch 'next' of github.com:diced/zipline into next 2021-02-11 20:49:08 -08:00
diced
6ab39fb94d 2.8.0 2021-02-11 20:48:12 -08:00
diced
e520f1e589 Vulnerability Fix 2021-02-11 20:47:57 -08:00
dicedtomato
f008a492a3 add video to upload 2021-02-07 09:59:48 -08:00
diced
eff47404df 2.7.2 2021-01-30 12:12:10 -08:00
diced
844dd2d4ed yes 2021-01-30 12:12:05 -08:00
diced
2ab17aa297 fix(api): file path with unknown ext 2021-01-30 12:11:52 -08:00
diced
776b0aa3c4 2.7.1 2021-01-30 08:19:27 -08:00
diced
701e5ae2d0 fix #59 2021-01-30 08:18:59 -08:00
diced
1529eb3afd fix #58 2021-01-30 08:18:50 -08:00
diced
d3676c2662 2.7.0 2021-01-29 22:13:58 -08:00
diced
cb93df347c fix(webhook): user 2021-01-29 22:13:43 -08:00
dicedtomato
c31a2172eb Merge pull request #56 from diced/dependabot/npm_and_yarn/ini-1.3.8
Bump ini from 1.3.5 to 1.3.8
2020-12-21 13:33:37 -08:00
dicedtomato
e3d0f5e47d Update docker.yml 2020-12-21 13:27:56 -08:00
dicedtomato
975fc00fad Update docker.yml 2020-12-21 13:23:50 -08:00
dicedtomato
03475bd7d7 Create docker.yml 2020-12-21 13:23:03 -08:00
diced
6992b0eb67 2.6.1 2020-12-15 14:32:11 -08:00
diced
c796927b35 paths 2020-12-15 14:32:00 -08:00
diced
00eb6aef41 2.6.0 2020-12-15 14:05:57 -08:00
diced
39f2773703 plugins fix 2020-12-15 14:05:42 -08:00
dependabot[bot]
59ce5e5cce Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-15 21:42:13 +00:00
diced
a0360269b8 why 2020-12-15 13:40:35 -08:00
diced
6270c725dc 2.5.8 2020-12-06 15:32:05 -08:00
diced
b82a50ae4e use last ext 2020-12-06 15:31:54 -08:00
diced
19d2c63ea9 2.5.7 2020-11-25 21:04:33 -08:00
diced
70b5ff7562 no file type? 2020-11-25 21:04:24 -08:00
dicedtomato
15367f0495 Update README.md 2020-11-20 11:50:11 -08:00
diced
306b8812e2 2.5.5 2020-11-12 21:57:00 -08:00
diced
06c57851e4 fix bug 2020-11-12 21:56:53 -08:00
diced
0063c97e01 2.5.4 2020-11-12 21:30:06 -08:00
diced
cdf7af0283 shorten urling 2020-11-12 21:25:05 -08:00
diced
1b4c8373ea stats 2020-11-12 20:53:58 -08:00
diced
585b737c5f make zipline volume use custom dir 2020-11-12 10:08:34 -08:00
diced
b1cfed834c make starting msg under log 2020-11-12 10:01:30 -08:00
diced
ccde532880 fixes 2020-11-12 09:58:52 -08:00
diced
bc7a7a8c24 enforcing single line if statements 2020-11-12 09:42:09 -08:00
diced
27ec3de3ac add config option for logging 2020-11-12 09:30:44 -08:00
diced
96c0a72178 phase out using Error into normal requests 2020-11-12 09:26:46 -08:00
diced
dafbe647af route logging 2020-11-12 09:26:24 -08:00
diced
b679239109 notify invalid configs 2020-11-11 22:10:47 -08:00
diced
5de1293949 fix disabling mfa 2020-11-11 21:59:04 -08:00
diced
134ee7f38e new 404 2020-11-11 21:55:38 -08:00
diced
0c365d6364 updated logging 2020-11-11 21:32:50 -08:00
diced
12ad45387a 2.5.3 2020-11-11 13:13:15 -08:00
diced
f6d62388fa fix no image path 2020-11-11 13:13:07 -08:00
45 changed files with 3242 additions and 1930 deletions

View File

@@ -29,6 +29,7 @@ module.exports = {
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'comma-dangle': ['error', 'never']
'comma-dangle': ['error', 'never'],
'nonblock-statement-body-position': ['error', 'beside']
}
};

21
.github/workflows/docker-hub.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Publish Zipline DockerHub Image
on:
push:
branches: [next]
pull_request:
branches: [next]
jobs:
push_to_registry:
name: Push Docker Image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to Docker Hub
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: diced/zipline
dockerfile: Dockerfile
tag_with_ref: true

22
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Publish Zipline Docker Image
on:
push:
branches: [next]
pull_request:
branches: [next]
jobs:
push_to_registry:
name: Push Docker Image to Github Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to GitHub Packages
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: diced/zipline/zipline
dockerfile: Dockerfile
tag_with_ref: true

View File

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

View File

@@ -0,0 +1,9 @@
version: "3"
services:
zipline:
ports:
- "8000:8000"
volumes:
- "./uploads:/opt/zipline/uploads"
build: .
tty: true

View File

@@ -1,6 +1,6 @@
{
"name": "zipline-next",
"version": "2.5.2",
"version": "2.9.0",
"private": true,
"dependencies": {
"@dicedtomato/colors": "^1.0.3",
@@ -40,10 +40,13 @@
"scripts": {
"format": "prettier --write .",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"dev": "ts-node src",
"dev:verbose": "VERBOSE=true ts-node src",
"build": "next build && tsc -p .",
"start": "NODE_ENV=production node dist",
"start:powershell": "$env:NODE_ENV='production'; node dist",
"start:powershell:verbose": "$env:NODE_ENV='production'; $env:VERBOSE='true'; node dist",
"start:verbose": "NODE_ENV=production VERBOSE=true node dist"
},
"devDependencies": {
@@ -61,6 +64,7 @@
"eslint-plugin-react": "^7.21.3",
"mongodb": "^3.6.2",
"prettier": "2.1.2",
"release": "^6.3.0",
"ts-node": "^9.0.0",
"typescript": "^4.0.3"
}

View File

@@ -4,14 +4,14 @@ const { stringify } = require('toml-patch');
const { writeFileSync } = require('fs');
const { join } = require('path');
const createDockerCompose = port => {
const createDockerCompose = (port, dir) => {
return `version: "3"
services:
zipline:
ports:
- "${port}:${port}"
volumes:
- "${join(process.cwd(), 'uploads')}:/opt/zipline/uploads"
- "${join(process.cwd(), dir)}:${join('/opt/zipline', dir)}"
build: .
tty: true`;
};
@@ -48,14 +48,14 @@ const base = {
{
type: 'list',
name: 'type',
message: 'What database type?',
message: 'What database type? (you will have to install the drivers)',
choices: [
{ name: 'postgres', extra: 'This is what we recomend using.' },
{ name: 'cockroachdb' },
{ name: 'mysql' },
{ name: 'mariadb' },
{ name: 'mssql' },
{ name: 'sqlite3' }
{ name: 'sqlite' }
]
},
{
@@ -103,14 +103,14 @@ const base = {
name: 'theme',
message: 'Theme',
choices: [
{ name: 'Dark Theme (recomended)' },
{ name: 'Light Theme (warning for eyes)' }
{ name: 'dark' },
{ name: 'light' }
]
},
{
type: 'confirm',
name: 'mfa',
message: 'Enable MFA with Authy/Google Authenticator'
message: 'Enable 2 Factor Authentication with Authy/Google Authenticator'
}
]);
@@ -160,11 +160,12 @@ const base = {
if (docker.useDocker) {
config.core.host = '0.0.0.0';
console.log('Generating docker-compose.yml...');
writeFileSync('docker-compose.yml', createDockerCompose(config.core.port));
writeFileSync('docker-compose.yml', createDockerCompose(config.core.port, config.uploader.directory));
console.log(
'Head to https://zipline.diced.wtf/docs/docker to learn how to run with docker.'
);
}
if (config.database.type !== "postgres") console.log(`please head to https://zipline.diced.wtf/docs/config/getting-started#database to see what drivers you need to install for ${config.database.type}`);
writeFileSync('Zipline.toml', stringify(config));
})();

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import Typography from '@material-ui/core/Typography';
import Card from '@material-ui/core/Card';
import Paper from '@material-ui/core/Paper';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Dialog from '@material-ui/core/Dialog';
@@ -12,12 +12,15 @@ 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 Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import Alert from '@material-ui/lab/Alert';
import { makeStyles } from '@material-ui/core';
import { UPDATE_USER } from '../reducer';
import { SET_THEME, UPDATE_USER } from '../reducer';
import { store } from '../store';
import { useDispatch } from 'react-redux';
import { Config } from '../lib/Config';
import { useRouter } from 'next/router';
const useStyles = makeStyles({
margin: {
@@ -37,7 +40,12 @@ const useStyles = makeStyles({
export default function ManageUser({ config }: { config: Config }) {
const classes = useStyles();
const dispatch = useDispatch();
const router = useRouter();
const state = store.getState();
const [theme, setTheme] = useState(
state.theme === '' ? 'dark-dark' : state.theme
);
const [alertOpen, setAlertOpen] = useState(false);
const [mfaDialogOpen, setMfaDialogOpen] = useState(false);
const [qrcode, setQRCode] = useState(null);
@@ -66,6 +74,12 @@ export default function ManageUser({ config }: { config: Config }) {
}
};
const handleUpdateTheme = evt => {
setTheme(evt.target.value);
dispatch({ type: SET_THEME, payload: evt.target.value });
router.replace('/user/manage');
};
const disableMFA = async () => {
await fetch('/api/mfa/disable');
const d = await (await fetch('/api/user')).json();
@@ -98,6 +112,12 @@ export default function ManageUser({ config }: { config: Config }) {
}
};
const disableMfaAndClose = async () => {
const d = await (await fetch('/api/mfa/disable')).json();
if (!d.error) setMfaDialogOpen(false);
};
return (
<React.Fragment>
<Snackbar
@@ -115,7 +135,7 @@ export default function ManageUser({ config }: { config: Config }) {
</Snackbar>
<Dialog
open={mfaDialogOpen}
onClose={() => setMfaDialogOpen(false)}
onClose={disableMfaAndClose}
aria-labelledby='enable-mfa'
aria-describedby='mfa-desc'
>
@@ -143,16 +163,29 @@ export default function ManageUser({ config }: { config: Config }) {
</Grid>
</DialogContent>
<DialogActions>
<Button color='primary' autoFocus onClick={disableMfaAndClose}>
Disable
</Button>
<Button color='primary' autoFocus onClick={tryEnablingMfa}>
Enable
</Button>
</DialogActions>
</Dialog>
<Card>
<Paper>
<CardContent>
<Typography color='textSecondary' variant='h4' gutterBottom>
Manage
</Typography>
<Select
id='select-theme-zipline'
value={theme}
onChange={handleUpdateTheme}
className={classes.field}
>
<MenuItem value={'dark-dark'}>Very Dark</MenuItem>
<MenuItem value={'blue-dark'}>Dark Blue</MenuItem>
<MenuItem value={'light'}>Light</MenuItem>
</Select>
<TextField
label='Username'
className={classes.field}
@@ -177,6 +210,13 @@ export default function ManageUser({ config }: { config: Config }) {
/>
</CardContent>
<CardActions>
<Button
className={classes.button}
color='primary'
onClick={handleUpdateTheme}
>
settheme
</Button>
<Button
className={classes.button}
color='primary'
@@ -194,7 +234,7 @@ export default function ManageUser({ config }: { config: Config }) {
</Button>
) : null}
</CardActions>
</Card>
</Paper>
</React.Fragment>
);
}

View File

@@ -58,13 +58,13 @@ const useStyles = makeStyles(theme => ({
},
appBar: {
display: 'flex',
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#fff',
color: theme.palette.type !== 'dark' ? '#000' : '#fff',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
...theme.overrides.MuiAppBar.root,
[theme.breakpoints.up('sm')]: {
width: 'calc(100%)',
marginLeft: drawerWidth
},
borderBottom: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0'
}
},
menuButton: {
marginRight: theme.spacing(2),
@@ -188,7 +188,9 @@ export default function UI({ children }) {
color='inherit'
className={classes.rightButton}
>
<Avatar src={`https://www.gravatar.com/avatar/${emailHash}.jpg`}>
<Avatar
src={`https://www.gravatar.com/avatar/${emailHash}.jpg`}
>
{state.user.username[0].toUpperCase()}
</Avatar>
</Button>
@@ -222,9 +224,7 @@ export default function UI({ children }) {
onClose={() => setAnchorEl(null)}
>
<NoFocusMenuItem>
<Typography variant='h6'>
{state.user.username}
</Typography>
<Typography variant='h6'>{state.user.username}</Typography>
</NoFocusMenuItem>
<Divider />
<Link href='/user/manage'>

View File

@@ -1,13 +1,21 @@
import React from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/core/styles';
import dark from '../lib/themes/dark';
import darkdark from '../lib/themes/darkdark';
import bluedark from '../lib/themes/bluedark';
import light from '../lib/themes/light';
import { useState } from 'react';
export default function ZiplineTheming({ Component, pageProps, theme }) {
console.log(theme);
const thm = {
'dark-dark': darkdark,
light: light,
'blue-dark': bluedark,
'': darkdark
};
return (
<ThemeProvider theme={theme == 'light' ? light : dark}>
<ThemeProvider theme={thm[theme]}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>

View File

@@ -1,55 +1,29 @@
import next from 'next';
import { textSync as text } from 'figlet';
import fastify, { FastifyReply, FastifyRequest } from 'fastify';
import fastifyTypeorm from 'fastify-typeorm-plugin';
import fastifyCookies from 'fastify-cookie';
import fastifyMultipart from 'fastify-multipart';
import fastifyRateLimit from 'fastify-rate-limit';
import fastify from 'fastify';
import fastifyStatic from 'fastify-static';
import fastifyFavicon from 'fastify-favicon';
import { bootstrap } from 'fastify-decorators';
import fastifyTypeorm from 'fastify-typeorm-plugin';
import { Console } from './lib/logger';
import { AddressInfo } from 'net';
import { magenta, bold, green, reset, blue, red } from '@dicedtomato/colors';
import { bold, green, reset } from '@dicedtomato/colors';
import { Configuration } from './lib/Config';
import { UserController } from './lib/api/controllers/UserController';
import { RootController } from './lib/api/controllers/RootController';
import { join } from 'path';
import { ImagesController } from './lib/api/controllers/ImagesController';
import { URLSController } from './lib/api/controllers/URLSController';
import { checkVersion } from './lib/Util';
import { existsSync, readFileSync } from 'fs';
import { Image } from './lib/entities/Image';
import { User } from './lib/entities/User';
import { Zipline } from './lib/entities/Zipline';
import { URL } from './lib/entities/URL';
import { MultiFactorController } from './lib/api/controllers/MultiFactorController';
import { PluginLoader } from './lib/plugin';
import { readdirSync, statSync } from 'fs';
const dev = process.env.NODE_ENV !== 'production';
const server = fastify({});
const app = next({
dev,
quiet: dev
});
(async () => {
if (await checkVersion())
Console.logger('Zipline').info(
'running an outdated version of zipline, please update soon!'
);
})();
console.log(`
${magenta(text('Zipline'))}
Version : ${blue(
process.env.npm_package_version ||
JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
.version
)}
GitHub : ${blue('https://github.com/ZiplineProject/zipline')}
Issues : ${blue('https://github.com/ZiplineProject/zipline/issues')}
Docs : ${blue('https://zipline.diced.wtf/')}
Mode : ${bold(dev ? red('dev') : green('production'))}
Verbose : ${bold(process.env.VERBOSE ? red('yes') : green('no'))}
`);
app.prepare();
const pluginLoader = new PluginLoader(server, process.cwd(), dev ? './src/plugins' : './dist/plugins');
Console.logger(Configuration).verbose('searching for config...');
const config = Configuration.readConfig();
if (!config) {
Console.logger(Configuration).error(
`could not find a Zipline.toml file in ${process.cwd()}`
@@ -57,156 +31,72 @@ if (!config) {
process.exit(0);
}
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
(async () => {
const builtInPlugins = await pluginLoader.loadPlugins(true);
for (const plugin of builtInPlugins) {
try {
plugin.onLoad(server, null, app, config);
} catch (e) {
Console.logger(PluginLoader).error(`failed to load built-in plugin: ${plugin.name}, ${e.message}`);
process.exit(0);
}
}
const server = fastify({});
const app = next({
dev,
quiet: dev
});
const handle = app.getRequestHandler();
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
const handle = app.getRequestHandler();
Console.logger(next).info('Preparing app...');
app.prepare();
Console.logger(next).verbose('Prepared app');
server.register(fastifyRateLimit, {
timeWindow: 5000,
max: 1,
global: false
});
if (dev)
server.get('/_next/*', async (req, reply) => {
await handle(req.raw, reply.raw);
return (reply.sent = true);
});
server.all('/*', async (req, reply) => {
await handle(req.raw, reply.raw);
return (reply.sent = true);
});
server.setNotFoundHandler(async (req, reply) => {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
});
server.get(`${config.urls.route}/:id`, async function (
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
const urls = this.orm.getRepository(URL);
const urlId = await urls.findOne({
where: {
id: req.params.id
server.get('/*', async (req, reply) => {
const routeRegex = /\/_next\/static|\/((dash|user)(\/)?(.+)?)?/gi;
if (routeRegex.test(req.url)) {
await handle(req.raw, reply.raw);
return (reply.sent = true);
} else {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
});
const urlVanity = await urls.findOne({
where: {
vanity: req.params.id
server.setNotFoundHandler(async (req, reply) => {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
});
server.register(fastifyStatic, {
root: path,
prefix: config.uploader.route
});
// done after everything so plugins can overwrite routes, etc.
server.register(async () => {
const plugins = await pluginLoader.loadPlugins();
for (const plugin of plugins) {
try {
plugin.onLoad(server, server.orm, app, config);
Console.logger(PluginLoader).info(`loaded plugin: ${plugin.name}`);
} catch (e) {
Console.logger(PluginLoader).error(`failed to load plugin: ${plugin.name}, ${e.message}`);
}
}
});
if (config.urls.vanity && urlVanity) return reply.redirect(urlVanity.url);
if (!urlId) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
return reply.redirect(urlId.url);
});
server.listen(
{
port: config.core.port,
host: config.core.host
},
async err => {
if (err) throw err;
const info = server.server.address() as AddressInfo;
server.get(`${config.uploader.rich_content_route || '/a'}/:id`, async function (
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
if (!existsSync(join(config.uploader.directory, req.params.id))) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
return reply.type('text/html').send(`
<html>
<head>
<meta property="theme-color" content="${config.meta.color}">
<meta property="og:title" content="${req.params.id}">
<meta property="og:url" content="${config.uploader.route}/${req.params.id}">
<meta property="og:image" content="${config.uploader.route}/${req.params.id}">
<meta property="twitter:card" content="summary_large_image">
</head>
<body>
<div style="text-align:center;vertical-align:middle;">
<img src="${config.uploader.route}/${req.params.id}" >
</div>
</body>
</html>
`);
});
server.register(fastifyMultipart);
server.register(fastifyTypeorm, {
...config.database,
entities: [Image, URL, User, Zipline],
synchronize: true,
logging: false
});
server.register(bootstrap, {
controllers: [
UserController,
RootController,
ImagesController,
URLSController,
MultiFactorController
]
});
server.register(fastifyCookies, {
secret: config.core.secret
});
server.register(fastifyStatic, {
root: path,
prefix: config.uploader.route
});
server.register(fastifyStatic, {
root: join(process.cwd(), 'public'),
prefix: '/public',
decorateReply: false
});
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;
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)
) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
});
);
}
);
})();

View File

@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
import { resolve } from 'path';
import { parse } from 'toml-patch';
import { ConnectionOptions } from 'typeorm';
import { WebhookHelper, WebhookType } from './Webhooks';
import { Webhooks, WebhookType } from './Webhooks';
export interface Config {
database: ConnectionOptions;
@@ -49,6 +49,7 @@ export interface ConfigCore {
ratelimiter?: ConfigCoreRateLimiter;
theme?: 'dark' | 'light';
mfa?: boolean;
log?: boolean;
}
export interface ConfigWebhooks {
@@ -78,8 +79,7 @@ export class Configuration {
try {
const data = readFileSync(resolve(process.cwd(), 'Zipline.toml'), 'utf8');
const parsed = parse(data);
if (parsed.webhooks)
parsed.webhooks.events = WebhookHelper.convert(parsed.webhooks.events);
if (parsed.webhooks) parsed.webhooks.events = Webhooks.convert(parsed.webhooks.events);
return parsed;
} catch (e) {
return null;

View File

@@ -6,6 +6,7 @@ import { compare } from 'semver';
import { Zipline } from './entities/Zipline';
import { readFileSync } from 'fs';
import { join } from 'path';
import { FastifyReply } from 'fastify';
const config = Configuration.readConfig();
@@ -14,8 +15,7 @@ export function createRandomId(
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
) {
let result = '';
for (let i = 0; i < length; i++)
result += charset.charAt(Math.floor(Math.random() * charset.length));
for (let i = 0; i < length; i++) result += charset.charAt(Math.floor(Math.random() * charset.length));
return result;
}
@@ -58,4 +58,11 @@ export async function checkVersion(): Promise<boolean> {
const compared = compare(currentVersion, latestVersion);
return compared == 0 || compared == 1 ? false : true;
}
export async function sendError(reply: FastifyReply, error: string) {
return reply.code(400).send({
code: 400,
error
});
}

5
src/lib/WebUtil.ts Normal file
View File

@@ -0,0 +1,5 @@
export function createURL(href: string, route: string, file: string): string {
const t = new URL(href);
t.pathname = `${route}/${file}`;
return t.toString();
}

View File

@@ -46,7 +46,7 @@ export type WebhookSendText =
| 'user_delete'
| 'user_edit';
export class WebhookHelper {
export class Webhooks {
public static convert(strings: WebhookSendText[]) {
return strings.map(x => WebhookType[x.toUpperCase()]);
}
@@ -59,7 +59,7 @@ export class WebhookHelper {
public static parseContent(content: string, data: WebhookData) {
return content
.replace(WebhookParseTokens.IMAGE_ID, data.image?.id)
.replace(WebhookParseTokens.IMAGE_URL, `${data.host}${data.image?.file}`)
.replace(WebhookParseTokens.IMAGE_URL, data.host)
.replace(WebhookParseTokens.URL_ID, data.url?.id)
.replace(WebhookParseTokens.URL_URL, data.host + data.url?.id)
.replace(WebhookParseTokens.URL_VANITY, data.url?.vanity)
@@ -81,11 +81,11 @@ export class WebhookHelper {
},
body: JSON.stringify({
username: config.webhooks.username,
content: WebhookHelper.parseContent(content, data)
content: Webhooks.parseContent(content, data)
})
});
} catch (e) {
Console.logger(WebhookHelper).error(e);
Console.logger(Webhooks).error(e);
}
}
}

View File

@@ -1,7 +0,0 @@
export class MissingBodyData extends Error {}
export class LoginError extends Error {}
export class NotAdministratorError extends Error {}
export class UserExistsError extends Error {}
export class UserNotFoundError extends Error {}
export class UserCredentialsError extends Error {}
export class AuthError extends Error {}

View File

@@ -9,12 +9,11 @@ import {
DELETE
} from 'fastify-decorators';
import { Repository } from 'typeorm';
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';
import { Image } from '../entities/Image';
import { Configuration, ConfigWebhooks } from '../Config';
import { Console } from '../logger';
import { readBaseCookie, sendError } from '../Util';
import { Webhooks, WebhookType } from '../Webhooks';
const config = Configuration.readConfig();
@@ -24,11 +23,11 @@ export class ImagesController {
private instance!: FastifyInstance;
private images: Repository<Image> = this.instance.orm.getRepository(Image);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
private webhooks: ConfigWebhooks = Webhooks.conf(config);
@GET('/')
async allImages(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const images = await this.images.find({
where: {
@@ -44,7 +43,7 @@ export class ImagesController {
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const image = await this.images.findOne({
where: {
@@ -53,24 +52,30 @@ export class ImagesController {
}
});
if (!image) throw new Error('No image');
if (!image) return sendError(reply, 'No image');
this.images.delete({
id: req.params.id
});
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = join(dir.charAt(0) == '/' ? dir : join(process.cwd(), dir), image.file);
const dir = config.uploader.directory
? config.uploader.directory
: 'uploads';
const path = join(
dir.charAt(0) == '/' ? dir : join(process.cwd(), dir),
image.file
);
try {
unlinkSync(path);
Console.logger(Image).info(`image ${image.id} was deleted`);
if (this.webhooks.events.includes(WebhookType.DELETE_IMAGE))
WebhookHelper.sendWebhook(this.webhooks.upload.content, {
image,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.uploader.route}/`
});
if (this.webhooks.events.includes(WebhookType.DELETE_IMAGE)) Webhooks.sendWebhook(this.webhooks.upload.content, {
image,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${
config.uploader.route
}/`
});
return reply.send(image);
} catch (e) {
@@ -81,7 +86,7 @@ export class ImagesController {
@GET('/recent')
async recentImages(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const images = await this.images.find({
where: {
@@ -94,7 +99,7 @@ export class ImagesController {
@GET('/chunk')
async pages(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const images = await this.images.find({
where: {
@@ -108,7 +113,7 @@ export class ImagesController {
if (!f.length) return array;
return [f].concat(chunk(array.slice(size, array.length), size));
}
const chunks = chunk(images, 20);
const chunks = chunk(images, 26);
return reply.send(chunks);
}
}

View File

@@ -7,14 +7,18 @@ import {
POST
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { User } from '../../entities/User';
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';
import {
checkPassword,
createBaseCookie,
readBaseCookie,
sendError
} from '../Util';
import { Console } from '../logger';
import { WebhookType, Webhooks } from '../Webhooks';
import { Configuration, ConfigWebhooks } from '../Config';
const config = Configuration.readConfig();
@@ -25,11 +29,11 @@ export class MultiFactorController {
private users: Repository<User> = this.instance.orm.getRepository(User);
private logger: Console = Console.logger(User);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
private webhooks: ConfigWebhooks = Webhooks.conf(config);
@GET('/qrcode')
async qrcode(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
let user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
@@ -62,7 +66,7 @@ export class MultiFactorController {
@GET('/disable')
async disable(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
@@ -73,6 +77,7 @@ export class MultiFactorController {
this.users.save(user);
this.logger.info(`disabled mfa ${user.username} (${user.id})`);
reply.send({ disabled: true });
}
@@ -84,9 +89,9 @@ export class MultiFactorController {
}>,
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.');
if (req.cookies.zipline) return sendError(reply, 'Already logged in.');
if (!req.body.username) return sendError(reply, 'Missing username.');
if (!req.body.password) return sendError(reply, 'Missing uassword.');
const user = await this.users.findOne({
where: {
@@ -94,13 +99,12 @@ export class MultiFactorController {
}
});
if (!user)
throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
if (!user) return sendError(reply, `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`
`${user.username} (${user.id}) tried to login but failed with mfa`
);
throw new LoginError('Wrong credentials!');
return sendError(reply, 'Wrong credentials!');
}
delete user.password;
@@ -116,12 +120,37 @@ export class MultiFactorController {
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
});
this.logger.info(`${user.username} (${user.id}) logged in with mfa`);
if (this.webhooks.events.includes(WebhookType.LOGIN)) Webhooks.sendWebhook(this.webhooks.login.content, {
user
});
return reply.send({ user, passed });
}
@GET('/verify')
async verifyOn(
req: FastifyRequest<{
Querystring: { token: string };
}>,
reply: FastifyReply
) {
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
}
});
if (!user) return sendError(reply, 'User that was signed in was not found, and guess what you should probably clear your cookies.');
const passed = totp.verify({
encoding: 'base32',
token: req.query.token,
secret: user.secretMfaKey
});
return reply.send(passed);
}
}

View File

@@ -12,16 +12,21 @@ 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 '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { createRandomId, getFirst } from '../../Util';
import { Console } from '../../logger';
import { WebhookHelper, WebhookType } from '../../Webhooks';
import { Image } from '../entities/Image';
import { User } from '../entities/User';
import { Configuration, ConfigWebhooks } from '../Config';
import { createRandomId, getFirst, sendError } from '../Util';
import { Console } from '../logger';
import { Webhooks, WebhookType } from '../Webhooks';
const pump = promisify(pipeline);
const config = Configuration.readConfig();
if (!config.core || !config.database) {
Console.logger('Zipline').error(
'configuration seems to be invalid, did you generate a config? https://zipline.diced.wtf/docs/auto'
);
process.exit(0);
}
const rateLimiterConfig = config.core.ratelimiter
? {
config: {
@@ -40,7 +45,7 @@ export class RootController {
private users: Repository<User> = this.instance.orm.getRepository(User);
private images: Repository<Image> = this.instance.orm.getRepository(Image);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
private webhooks: ConfigWebhooks = Webhooks.conf(config);
private logger: Console = Console.logger(Image);
@GET('/first')
@@ -56,7 +61,7 @@ export class RootController {
@GET('/users')
async allUsers(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new Error('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const users = await this.users.find();
const final = [];
@@ -71,7 +76,7 @@ export class RootController {
@GET('/statistics')
async stats(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new Error('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const images = await this.images.find();
const users = await this.users.find();
@@ -104,34 +109,32 @@ export class RootController {
@POST('/upload', rateLimiterConfig)
async loginStatus(req: FastifyRequest, reply: FastifyReply) {
if (!req.headers.authorization)
return new AuthError('No authorization header!');
if (!req.headers.authorization) return sendError(reply, 'No authorization header!');
const user = await this.users.findOne({
where: {
token: req.headers.authorization
}
});
if (!user) return new AuthError('Incorrect token!');
if (!user) return sendError(reply, 'Incorrect token!');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore stupid multipart types smh
const data: Multipart = await req.file();
if (!existsSync(config.uploader.directory))
mkdirSync(config.uploader.directory);
const ext = data.filename.split('.')[1];
if (config.uploader.blacklisted.includes(ext))
throw new Error('Blacklisted file extension!');
if (!existsSync(config.uploader.directory)) mkdirSync(config.uploader.directory);
const og = data.filename;
const ext = data.filename.split('.').pop();
if (config.uploader.blacklisted.includes(ext)) return sendError(reply, 'Blacklisted file extension!');
console.log(data.filename);
const fileName = config.uploader.original
? data.filename.split('.')[0]
? og
: createRandomId(config.uploader.length);
const path = join(config.uploader.directory, `${fileName}.${ext}`);
const path = join(config.uploader.directory, config.uploader.original ? fileName : `${fileName}.${ext}`);
this.logger.verbose(`attempting to save ${fileName} to db`);
const image = await this.images.save(new Image(fileName, ext, user.id));
const image = await this.images.save(new Image(config.uploader.original, fileName, ext, user.id));
this.logger.verbose(`saved image ${image.id} to db`);
this.logger.verbose(`attempting to save file ${path}`);
@@ -146,13 +149,13 @@ export class RootController {
config.uploader.rich_content_route
? config.uploader.rich_content_route
: config.uploader.route
}/`;
}/${config.uploader.original ? og : `${fileName}.${ext}`}`;
if (this.webhooks.events.includes(WebhookType.UPLOAD))
WebhookHelper.sendWebhook(this.webhooks.upload.content, {
image,
host
});
if (this.webhooks.events.includes(WebhookType.UPLOAD)) Webhooks.sendWebhook(this.webhooks.upload.content, {
image,
user,
host
});
reply.send(host);
}

View File

@@ -8,13 +8,12 @@ import {
POST
} from 'fastify-decorators';
import { Repository } from 'typeorm';
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';
import { URL } from '../entities/URL';
import { User } from '../entities/User';
import { Configuration, ConfigWebhooks } from '../Config';
import { Console } from '../logger';
import { createRandomId, readBaseCookie, sendError } from '../Util';
import { WebhookType, Webhooks } from '../Webhooks';
const config = Configuration.readConfig();
@@ -26,11 +25,11 @@ export class URLSController {
private urls: Repository<URL> = this.instance.orm.getRepository(URL);
private users: Repository<User> = this.instance.orm.getRepository(User);
private logger: Console = Console.logger(URL);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
private webhooks: ConfigWebhooks = Webhooks.conf(config);
@GET('/')
async allURLS(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const all = await this.urls.find({
where: {
@@ -46,7 +45,7 @@ export class URLSController {
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const url = await this.urls.findOne({
where: {
@@ -55,7 +54,7 @@ export class URLSController {
}
});
if (!url) throw new Error('No url');
if (!url) return sendError(reply, 'No url');
this.logger.verbose(`attempting to delete url ${url.id}`);
this.urls.delete({
@@ -63,11 +62,12 @@ export class URLSController {
});
this.logger.info(`url ${url.id} was deleted`);
if (this.webhooks.events.includes(WebhookType.DELETE_URL))
WebhookHelper.sendWebhook(this.webhooks.delete_url.content, {
url,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.urls.route}/`
});
if (this.webhooks.events.includes(WebhookType.DELETE_URL)) Webhooks.sendWebhook(this.webhooks.delete_url.content, {
url,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${
config.urls.route
}/`
});
return reply.send(url);
}
@@ -77,7 +77,7 @@ export class URLSController {
req: FastifyRequest<{ Body: { vanity: string; url: string } }>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
if (config.urls.vanity && req.body.vanity) {
const existingVanity = await this.urls.findOne({
@@ -85,7 +85,7 @@ export class URLSController {
vanity: req.body.vanity
}
});
if (existingVanity) throw new Error('There is an existing vanity!');
if (existingVanity) return sendError(reply, 'There is an existing vanity!');
}
const user = await this.users.findOne({
@@ -94,7 +94,7 @@ export class URLSController {
}
});
if (!user) throw new LoginError('No user');
if (!user) return sendError(reply, 'No user');
const id = createRandomId(config.urls.length);
@@ -104,11 +104,12 @@ export class URLSController {
);
this.logger.info(`saved url ${url.id}`);
if (this.webhooks.events.includes(WebhookType.SHORTEN))
WebhookHelper.sendWebhook(this.webhooks.shorten.content, {
url,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${config.urls.route}/`
});
if (this.webhooks.events.includes(WebhookType.SHORTEN)) Webhooks.sendWebhook(this.webhooks.shorten.content, {
url,
host: `${config.core.secure ? 'https' : 'http'}://${req.hostname}${
config.urls.route
}/`
});
return reply.send(url);
}

View File

@@ -9,25 +9,21 @@ import {
DELETE
} from 'fastify-decorators';
import { Repository } from 'typeorm';
import { User } from '../../entities/User';
import { Zipline } from '../../entities/Zipline';
import {
UserNotFoundError,
MissingBodyData,
LoginError,
UserExistsError
} from '../APIErrors';
import { Configuration, ConfigWebhooks } from '../../Config';
import { Console } from '../../logger';
import { User } from '../entities/User';
import { Image } from '../entities/Image';
import { Zipline } from '../entities/Zipline';
import { Configuration, ConfigWebhooks } from '../Config';
import { Console } from '../logger';
import {
checkPassword,
createBaseCookie,
createToken,
encryptPassword,
getFirst,
readBaseCookie
} from '../../Util';
import { WebhookType, WebhookHelper } from '../../Webhooks';
readBaseCookie,
sendError
} from '../Util';
import { WebhookType, Webhooks } from '../Webhooks';
const config = Configuration.readConfig();
@@ -37,8 +33,9 @@ export class UserController {
private instance!: FastifyInstance;
private users: Repository<User> = this.instance.orm.getRepository(User);
private images: Repository<Image> = this.instance.orm.getRepository(Image);
private logger: Console = Console.logger(User);
private webhooks: ConfigWebhooks = WebhookHelper.conf(config);
private webhooks: ConfigWebhooks = Webhooks.conf(config);
@GET('/login-status')
async loginStatus(req: FastifyRequest, reply: FastifyReply) {
@@ -49,18 +46,42 @@ export class UserController {
@GET('/')
async currentUser(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
}
});
// eslint-disable-next-line quotes
if (!user) throw new UserExistsError("User doesn't exist");
if (!user) return sendError(reply, "User doesn't exist");
delete user.password;
return reply.send(user);
}
@GET('/stats')
async stats(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const user = await this.users.findOne({
where: {
id: readBaseCookie(req.cookies.zipline)
}
});
const images = await this.images.find({
where: {
user: user.id
}
});
const totalViews = images.map(x => x.views).reduce((a, b) => Number(a) + Number(b), 0);
return reply.send({
totalViews,
images: images.length,
averageViews: totalViews / images.length
});
}
@PATCH('/')
async editUser(
req: FastifyRequest<{
@@ -68,7 +89,7 @@ export class UserController {
}>,
reply: FastifyReply
) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
const user = await this.users.findOne({
where: {
@@ -76,7 +97,7 @@ export class UserController {
}
});
// eslint-disable-next-line quotes
if (!user) throw new UserExistsError("User doesn't exist");
if (!user) return sendError(reply, "User doesn't exist");
this.logger.verbose(`attempting to save ${user.username} (${user.id})`);
user.username = req.body.username;
@@ -85,10 +106,9 @@ export class UserController {
await this.users.save(user);
this.logger.info(`saved ${user.username} (${user.id})`);
if (this.webhooks.events.includes(WebhookType.USER_EDIT))
WebhookHelper.sendWebhook(this.webhooks.user_edit.content, {
user
});
if (this.webhooks.events.includes(WebhookType.USER_EDIT)) Webhooks.sendWebhook(this.webhooks.user_edit.content, {
user
});
delete user.password;
return reply.send(user);
@@ -99,9 +119,9 @@ export class UserController {
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.');
if (req.cookies.zipline) return sendError(reply, 'Already logged in.');
if (!req.body.username) return sendError(reply, 'Missing username.');
if (!req.body.password) return sendError(reply, 'Missing uassword.');
const user = await this.users.findOne({
where: {
@@ -109,13 +129,12 @@ export class UserController {
}
});
if (!user)
throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
if (!user) return sendError(reply, `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!');
return sendError(reply, 'Wrong credentials!');
}
reply.send({
@@ -128,9 +147,9 @@ export class UserController {
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.');
if (req.cookies.zipline) return sendError(reply, 'Already logged in.');
if (!req.body.username) return sendError(reply, 'Missing username.');
if (!req.body.password) return sendError(reply, 'Missing uassword.');
const user = await this.users.findOne({
where: {
@@ -138,34 +157,33 @@ export class UserController {
}
});
if (!user)
throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
if (!user) return sendError(reply, `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!');
return sendError(reply, 'Wrong credentials!');
}
delete user.password;
this.logger.verbose(`set cookie for ${user.username} (${user.id})`);
reply.setCookie('zipline', createBaseCookie(user.id), {
path: '/',
maxAge: 1036800000
maxAge: 1036800000,
signed: true
});
this.logger.info(`${user.username} (${user.id}) logged in`);
if (this.webhooks.events.includes(WebhookType.LOGIN))
WebhookHelper.sendWebhook(this.webhooks.login.content, {
user
});
if (this.webhooks.events.includes(WebhookType.LOGIN)) Webhooks.sendWebhook(this.webhooks.login.content, {
user
});
return reply.send(user);
}
@POST('/logout')
async logout(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
try {
reply.clearCookie('zipline', { path: '/' });
return reply.send({ clearStore: true });
@@ -176,7 +194,6 @@ export class UserController {
@POST('/reset-token')
async resetToken(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError('Not logged in.');
const user = await this.users.findOne({
where: {
@@ -184,7 +201,7 @@ export class UserController {
}
});
if (!user) throw new UserNotFoundError('User was not found.');
if (!user) return sendError(reply, 'User was not found.');
this.logger.verbose(
`attempting to reset token ${user.username} (${user.id})`
@@ -193,10 +210,9 @@ export class UserController {
await this.users.save(user);
this.logger.info(`reset token ${user.username} (${user.id})`);
if (this.webhooks.events.includes(WebhookType.TOKEN_RESET))
WebhookHelper.sendWebhook(this.webhooks.token_reset.content, {
user
});
if (this.webhooks.events.includes(WebhookType.TOKEN_RESET)) Webhooks.sendWebhook(this.webhooks.token_reset.content, {
user
});
return reply.send({ updated: true });
}
@@ -208,13 +224,19 @@ export class UserController {
}>,
reply: FastifyReply
) {
if (!req.body.username) throw new MissingBodyData('Missing username.');
if (!req.body.password) throw new MissingBodyData('Missing uassword.');
const firstSetup = await getFirst(this.instance.orm);
if (!firstSetup && !req.cookies.zipline) return sendError(reply, 'Not logged in.');
if (!req.body.username) return sendError(reply, 'Missing username.');
if (!req.body.password) return sendError(reply, 'Missing uassword.');
const existing = await this.users.findOne({
where: { username: req.body.username }
});
if (existing) throw new UserExistsError('User exists already');
if (existing) return sendError(reply, 'User exists already');
if (req.body.username.length > 25) return sendError(reply, 'Limit 25');
try {
this.logger.verbose(`attempting to create ${req.body.username}`);
@@ -227,26 +249,23 @@ export class UserController {
)
);
this.logger.info(`created user ${user.username} (${user.id})`);
if (this.webhooks.events.includes(WebhookType.CREATE_USER))
WebhookHelper.sendWebhook(this.webhooks.create_user.content, {
user
});
if (this.webhooks.events.includes(WebhookType.CREATE_USER)) Webhooks.sendWebhook(this.webhooks.create_user.content, {
user
});
const firstSetup = await getFirst(this.instance.orm);
if (firstSetup)
await this.instance.orm.getRepository(Zipline).update(
{
id: 'zipline'
},
{
first: false
}
);
if (firstSetup) await this.instance.orm.getRepository(Zipline).update(
{
id: 'zipline'
},
{
first: false
}
);
delete user.password;
return reply.send(user);
} catch (e) {
throw new Error(`Could not create user: ${e.message}`);
return sendError(reply, `Could not create user: ${e.message}`);
}
}
@@ -262,7 +281,7 @@ export class UserController {
const existing = await this.users.findOne({
where: { id: req.params.id }
});
if (!existing) throw new UserExistsError('User doesnt exist');
if (!existing) return sendError(reply, 'User doesnt exist');
try {
this.logger.verbose(
@@ -271,14 +290,13 @@ export class UserController {
await this.users.remove(existing);
this.logger.info(`deleted ${existing.username} (${existing.id})`);
if (this.webhooks.events.includes(WebhookType.USER_DELETE))
WebhookHelper.sendWebhook(this.webhooks.user_delete.content, {
user: existing
});
if (this.webhooks.events.includes(WebhookType.USER_DELETE)) Webhooks.sendWebhook(this.webhooks.user_delete.content, {
user: existing
});
return reply.send({ ok: true });
} catch (e) {
throw new Error(`Could not delete user: ${e.message}`);
return sendError(reply, `Could not delete user: ${e.message}`);
}
}
// @Hook('preValidation')

View File

@@ -2,7 +2,7 @@ import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity({ name: 'zipline_images' })
export class Image {
@PrimaryColumn('text')
@PrimaryColumn('varchar', { length: 255 })
public id: string;
@Column('text', { default: null })
@@ -14,9 +14,10 @@ export class Image {
@Column('bigint', { default: '0' })
public views: number;
public constructor(id: string, ext: string, user: number) {
public constructor(original: boolean, id: string, ext: string, user: number) {
this.id = id;
this.file = `${id}.${ext}`;
if (original) this.file = id;
else this.file = `${id}.${ext}`;
this.user = user;
this.views = 0;
}

View File

@@ -1,5 +1,5 @@
import { ConsoleLevel } from '.';
import { blue, red, reset, white, yellow } from '@dicedtomato/colors';
import { blue, green, red, reset, white, yellow } from '@dicedtomato/colors';
export interface Formatter {
format(
@@ -27,7 +27,7 @@ export class DefaultFormatter implements Formatter {
level: ConsoleLevel,
time: Date
): string {
return `[${time.toLocaleString().replace(',', '')}] ${this.formatLevel(
return `[${time.toLocaleString().replace(',', '')}] [${green(origin.toLowerCase())}] ${this.formatLevel(
level
)} ${reset(message)}`;
}

11
src/lib/plugin/Plugin.ts Normal file
View File

@@ -0,0 +1,11 @@
import { FastifyInstance } from "fastify";
import Server from "next/dist/next-server/server/next-server";
import { Connection } from "typeorm";
import { Config } from "../Config";
export interface Plugin {
name: string;
priority?: number;
onLoad(server: FastifyInstance, orm: Connection, app: Server, config: Config): any;
}

View File

@@ -0,0 +1,45 @@
import { FastifyInstance } from 'fastify';
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { Plugin } from './Plugin';
export class PluginLoader {
public directory: string;
public files: string[];
public plugins: Plugin[] = [];
public builtIns: Plugin[] = [];
public fastify: FastifyInstance;
constructor(fastify: FastifyInstance, ...directory: string[]) {
this.directory = join(...directory);
this.fastify = fastify;
}
public getAllFiles(builtIn = false): string[] {
const result = [];
const r = (dir: string) => {
for (const file of readdirSync(dir)) {
const p = join(dir, file);
const s = statSync(p);
if (s.isDirectory()) r(p);
else result.push(p);
}
};
r(builtIn ? join(process.cwd(), process.env.NODE_ENV !== 'development' ? 'dist' : 'src', 'lib', 'plugin', 'builtins') : this.directory);
return result;
}
public async loadPlugins(builtIn = false): Promise<Plugin[]> {
const files = this.getAllFiles(builtIn);
for (const pluginFile of files) {
const im = await import(pluginFile);
builtIn ? this.builtIns.push(new im.default()) : this.plugins.push(new im.default());
}
return builtIn ? this.builtIns.sort((a, b) => a.priority - b.priority) : this.plugins.sort((a, b) => a.priority - b.priority);
}
}

View File

@@ -0,0 +1,59 @@
import { FastifyInstance } from 'fastify';
import Server from 'next/dist/next-server/server/next-server';
import { Connection } from 'typeorm';
import { Config } from '../../Config';
import { Plugin } from '../Plugin';
import fastifyTypeorm from 'fastify-typeorm-plugin';
import fastifyCookies from 'fastify-cookie';
import fastifyMultipart from 'fastify-multipart';
import fastifyRateLimit from 'fastify-rate-limit';
import fastifyStatic from 'fastify-static';
import fastifyFavicon from 'fastify-favicon';
import { bootstrap } from 'fastify-decorators';
import { User } from '../../entities/User';
import { Zipline } from '../../entities/Zipline';
import { Image } from '../../entities/Image';
import { URL } from '../../entities/URL';
import { UserController } from '../../controllers/UserController';
import path, { join } from 'path';
import { ImagesController } from '../../controllers/ImagesController';
import { MultiFactorController } from '../../controllers/MultiFactorController';
import { RootController } from '../../controllers/RootController';
import { URLSController } from '../../controllers/URLSController';
export default class implements Plugin {
public name: string = "assets";
public onLoad(server: FastifyInstance, orm: Connection, app: Server, config: Config) {
server.register(fastifyMultipart);
server.register(fastifyTypeorm, {
...config.database,
entities: [Image, URL, User, Zipline],
synchronize: true,
logging: false
});
server.register(bootstrap, {
controllers: [
UserController,
RootController,
ImagesController,
URLSController,
MultiFactorController
]
});
server.register(fastifyCookies, {
secret: config.core.secret
});
server.register(fastifyStatic, {
root: join(process.cwd(), 'public'),
prefix: '/public',
decorateReply: false
});
server.register(fastifyFavicon);
}
}

View File

@@ -0,0 +1,29 @@
import { FastifyInstance } from 'fastify';
import Server from 'next/dist/next-server/server/next-server';
import { Connection } from 'typeorm';
import { Config } from '../../Config';
import { Plugin } from '../Plugin';
import { textSync } from 'figlet';
import { magenta, blue, bold, red, green } from '@dicedtomato/colors';
import { readFileSync } from 'fs';
import { join } from 'path';
export default class implements Plugin {
public name: string = "assets";
public onLoad(server: FastifyInstance, orm: Connection, app: Server, config: Config) {
if (config.core.log) console.log(`
${magenta(textSync('Zipline'))}
Version : ${blue(
process.env.npm_package_version ||
JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
.version
)}
GitHub : ${blue('https://github.com/ZiplineProject/zipline')}
Issues : ${blue('https://github.com/ZiplineProject/zipline/issues')}
Docs : ${blue('https://zipline.diced.wtf/')}
Mode : ${bold(process.env.NODE_ENV !== 'production' ? red('dev') : green('production'))}
Verbose : ${bold(process.env.VERBOSE ? red('yes') : green('no'))}
`);
}
}

2
src/lib/plugin/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './Plugin';
export * from './PluginLoader';

View File

@@ -0,0 +1,49 @@
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
const blueDarkTheme = createMuiTheme({
palette: {
type: 'dark',
primary: {
main: '#fff'
},
secondary: {
main: '#4a5bb0'
},
background: {
default: '#0b1524',
paper: '#0a1930'
}
},
overrides: {
MuiListItem: {
root: {
'&$selected': {
backgroundColor: '#182f52'
}
}
},
MuiAppBar: {
root: {
borderBottom: '#1f1f1f',
backgroundColor: '#162946'
}
},
MuiPaper: {
outlined: {
borderColor: '#ffffff'
}
},
MuiCard: {
root: {
backgroundColor: '#182f52'
}
},
MuiButton: {
root: {
margin: '1320000'
}
}
}
});
export default blueDarkTheme;

View File

@@ -1,6 +1,6 @@
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
const darkTheme = createMuiTheme({
const darkdarkTheme = createMuiTheme({
palette: {
type: 'dark',
primary: {
@@ -22,6 +22,12 @@ const darkTheme = createMuiTheme({
}
}
},
MuiAppBar: {
root: {
borderBottom: '#1f1f1f',
backgroundColor: '#000000'
}
},
MuiCard: {
root: {
backgroundColor: '#080808'
@@ -35,4 +41,4 @@ const darkTheme = createMuiTheme({
}
});
export default darkTheme;
export default darkdarkTheme;

View File

@@ -22,6 +22,11 @@ const lightTheme = createMuiTheme({
}
}
},
MuiAppBar: {
root: {
borderBottom: '#1f1f1f'
}
},
MuiCard: {
root: {
backgroundColor: '#fff'

56
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import Head from 'next/head';
import Typography from '@material-ui/core/Typography';
import CssBaseline from '@material-ui/core/CssBaseline';
import Grid from '@material-ui/core/Grid';
import { ThemeProvider } from '@material-ui/core/styles';
import dark from '../lib/themes/darkdark';
export default function NotFound() {
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
}, []);
const faces = ['◉_◉', 'ರ_ರ'];
return (
<React.Fragment>
<Head>
<title>Not Found</title>
<meta
name='viewport'
content='minimum-scale=1, initial-scale=1, width=device-width'
/>
<link
rel='icon'
type='image/png'
href='https://twemoji.maxcdn.com/v/13.0.1/72x72/1f621.png'
/>
</Head>
<ThemeProvider theme={dark}>
<CssBaseline />
<Grid
container
spacing={0}
direction='column'
alignItems='center'
justify='center'
style={{ minHeight: '100vh' }}
>
<Grid item xs={6}>
<Typography variant='h2'>
<b>{faces[Math.floor(Math.random() * faces.length)]}</b>
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant='h4'>
<b>Looks like you&apos;ve hit a dead end...</b>
</Typography>
</Grid>
</Grid>
</ThemeProvider>
</React.Fragment>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import { Provider } from 'react-redux';
@@ -7,16 +7,12 @@ 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');
function App({ Component, pageProps }) {
const state = store.getState();
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
(async () => {
const d = await (await fetch('/api/theme')).json();
if (!d.error) setTheme(d.theme);
})();
}, []);
return (
<React.Fragment>
@@ -33,7 +29,7 @@ function MyApp({ Component, pageProps }) {
<ZiplineTheming
Component={Component}
pageProps={pageProps}
theme={theme}
theme={state.theme}
/>
</PersistGate>
</Provider>
@@ -41,9 +37,9 @@ function MyApp({ Component, pageProps }) {
);
}
MyApp.propTypes = {
App.propTypes = {
Component: PropTypes.elementType.isRequired,
pageProps: PropTypes.object.isRequired
};
export default MyApp;
export default App;

View File

@@ -3,7 +3,7 @@
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/core/styles';
import theme from '../lib/themes/dark';
import theme from '../lib/themes/darkdark';
import { Config, Configuration } from '../lib/Config';
export interface DocumentProps {

View File

@@ -12,6 +12,7 @@ import UI from '../components/UI';
import UIPlaceholder from '../components/UIPlaceholder';
import { makeStyles } from '@material-ui/core';
import { store } from '../store';
import { createURL } from '../lib/WebUtil';
import { Configuration } from '../lib/Config';
const useStyles = makeStyles(theme => ({
@@ -19,7 +20,6 @@ const useStyles = makeStyles(theme => ({
margin: '5px'
},
padding: {
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {
@@ -33,8 +33,12 @@ export default function Dashboard({ config }) {
const router = useRouter();
const state = store.getState();
const [loading, setLoading] = React.useState(true);
const [stats, setStats] = React.useState<{
totalViews: number;
averageViews: number;
images: number;
}>(null);
const [recentImages, setRecentImages] = React.useState([]);
const [images, setImages] = React.useState([]);
if (typeof window === 'undefined') return <UIPlaceholder />;
if (!state.loggedIn) router.push('/user/login');
@@ -44,9 +48,9 @@ export default function Dashboard({ config }) {
const recentImages = await (await fetch('/api/images/recent')).json();
if (!recentImages.error) setRecentImages(recentImages);
const allImages = await (await fetch('/api/images')).json();
if (!allImages.error) {
setImages(allImages);
const stats = await (await fetch('/api/user/stats')).json();
if (!stats.error) {
setStats(stats);
setLoading(false);
}
})();
@@ -59,17 +63,17 @@ export default function Dashboard({ config }) {
</Backdrop>
{!loading ? (
<Paper elevation={3} className={classes.padding}>
<Typography variant='h5'>
<Typography variant='h4'>
Welcome back, {state.user.username}
</Typography>
<Typography color='textSecondary'>
You have <b>{images.length}</b> images
You have <b>{stats.images}</b> images, with{' '}
<b>{stats.totalViews}</b> ({Math.round(stats.averageViews)})
collectively.
</Typography>
<Typography variant='h5'>Recent Images</Typography>
<Grid container spacing={2}>
{recentImages.map(d => {
const t = new URL(window.location.href);
t.pathname = `${config ? config.uploader.route : '/u'}/${d.file}`;
return (
<Grid item key={d.id} xs={12} sm={4}>
<Card>
@@ -77,7 +81,11 @@ export default function Dashboard({ config }) {
<CardMedia
component='img'
height='140'
image={t.toString()}
image={createURL(
window.location.href,
config ? config.uploader.route : '/u',
d.file
)}
/>
</CardActionArea>
</Card>
@@ -96,4 +104,4 @@ export default function Dashboard({ config }) {
export async function getStaticProps() {
const config = Configuration.readConfig();
return { props: { config: config } };
}
}

View File

@@ -18,13 +18,15 @@ import { makeStyles } from '@material-ui/core';
import { store } from '../../store';
import { Image } from '../../lib/entities/Image';
import { Configuration } from '../../lib/Config';
import { createURL } from '../../lib/WebUtil';
const useStyles = makeStyles(theme => ({
margin: {
margin: '5px'
},
padding: {
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
border:
theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
padding: '10px'
},
backdrop: {
@@ -56,6 +58,7 @@ export default function Images({ config }) {
const c = await (await fetch('/api/images/chunk')).json();
if (!c.error) {
setChunks(c);
console.log(c);
return c;
}
return [];
@@ -109,29 +112,29 @@ export default function Images({ config }) {
{showPagination ? (
<>
<Grid container spacing={2}>
{images.map(d => {
const t = new URL(window.location.href);
t.pathname = `${config ? config.uploader.route : '/u'}/${d.file}`;
return (
<Grid
item
xs={12}
sm={6}
key={d.id}
onClick={e => setImageOpenPopover(e, d)}
>
<Card>
<CardActionArea>
<CardMedia
component='img'
height='140'
image={t.toString()}
/>
</CardActionArea>
</Card>
</Grid>
);
})}
{images.map(d => (
<Grid
item
xs={12}
sm={4}
key={d.id}
onClick={e => setImageOpenPopover(e, d)}
>
<Card>
<CardActionArea>
<CardMedia
component='img'
height='140'
image={createURL(
window.location.href,
config ? config.uploader.route : '/u',
d.file
)}
/>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
<Pagination count={chunks.length} onChange={changePage} />
</>
@@ -143,7 +146,7 @@ export default function Images({ config }) {
alignItems='center'
justify='center'
>
<Grid item xs={6} sm={12}>
<Grid item xs>
<AddToPhotosIcon style={{ fontSize: 100 }} />
</Grid>
</Grid>
@@ -182,4 +185,4 @@ export default function Images({ config }) {
export async function getStaticProps() {
const config = Configuration.readConfig();
return { props: { config: config } };
}
}

View File

@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import React from 'react';
import { useRouter } from 'next/router';
import Typography from '@material-ui/core/Typography';
@@ -32,6 +35,8 @@ export default function Upload() {
const state = store.getState();
const [files, setFiles] = React.useState<File[]>([]);
const [alertOpen, setAlertOpen] = React.useState<boolean>(false);
const [alertSev, setAlertSev] = React.useState('success');
const [alertMsg, setAlertMsg] = React.useState('Uploaded Image!');
const handleFileUpload = async () => {
const file = files[0];
@@ -46,43 +51,59 @@ export default function Upload() {
body
});
if (res.ok) setAlertOpen(true);
if (res.ok) {
setAlertOpen(true);
setAlertMsg('Uploaded Image!');
setAlertSev('success');
} else {
const d = await res.json();
setAlertOpen(true);
setAlertMsg(`Couldn't upload: ${d.error}`);
setAlertSev('error');
}
};
const snack = (
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={alertOpen}
autoHideDuration={6000}
onClose={() => setAlertOpen(false)}
>
<Alert severity={alertSev} variant='filled'>
{alertMsg}
</Alert>
</Snackbar>
);
if (typeof window === 'undefined') return <UIPlaceholder />;
if (!state.loggedIn) router.push('/user/login');
else {
return (
<UI>
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={alertOpen}
autoHideDuration={6000}
onClose={() => setAlertOpen(false)}
>
<Alert severity='success' variant='filled'>
Uploaded image!
</Alert>
</Snackbar>
<Paper elevation={3} className={classes.padding}>
<Typography variant='h5'>
Upload
</Typography>
<Box m={1}>
<DropzoneArea
acceptedFiles={['image/*']}
dropzoneText={'Drag an image or click to upload an image.'}
onChange={f => setFiles(f)}
maxFileSize={1073741824} // 1gb in byte
/>
</Box>
<Button onClick={handleFileUpload}>Submit</Button>
</Paper>
<>
{snack}
<Paper elevation={3} className={classes.padding}>
<Typography variant='h5'>
Upload
</Typography>
<Box m={1}>
<DropzoneArea
acceptedFiles={['image/*', 'video/*']}
dropzoneText={'Drag an image or click to upload an image.'}
onChange={f => setFiles(f)}
filesLimit={1}
maxFileSize={1073741824} // 1gb in byte
/>
</Box>
<Button onClick={handleFileUpload}>Upload</Button>
</Paper>
</>
</UI>
);
}
return <UIPlaceholder />;
}
}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import Typography from '@material-ui/core/Typography';
import Backdrop from '@material-ui/core/Backdrop';
import CircularProgress from '@material-ui/core/CircularProgress';
@@ -23,9 +24,11 @@ import AddIcon from '@material-ui/icons/Add';
import copy from 'copy-to-clipboard';
import UI from '../../components/UI';
import UIPlaceholder from '../../components/UIPlaceholder';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import { makeStyles } from '@material-ui/core';
import { URL as URLEntity } from '../../lib/entities/URL';
import { Configuration } from '../../lib/Config';
import { createURL } from '../../lib/WebUtil';
const useStyles = makeStyles(theme => ({
margin: {
@@ -157,41 +160,46 @@ export default function Urls({ config }) {
<Typography variant='h5'>
URLs
<IconButton
aria-label='Create User'
aria-label='Create URL'
onClick={() => setCreateOpen(true)}
>
<AddIcon />
</IconButton>
</Typography>
<Grid container spacing={2}>
{urls.length > 0 ? urls.map(u => {
const url = new URL(window.location.href);
url.pathname = `${config ? config.urls.route : '/go'}/${u.id}`;
return (
<Grid item xs={12} sm={4} key={u.id}>
<Card elevation={3}>
<CardHeader
action={
<div>
<IconButton aria-label='Copy URL'>
<FileCopyIcon
onClick={() => copy(url.toString())}
/>
</IconButton>
<IconButton
aria-label='Delete Forever'
onClick={() => deleteUrl(u)}
>
<DeleteForeverIcon />
</IconButton>
</div>
}
title={u.vanity ? u.vanity : u.id}
/>
</Card>
</Grid>
);
}) : (
{urls.length > 0 ? urls.map(u => ((
<Grid item xs={12} sm={4} key={u.id}>
<Card elevation={3}>
<CardHeader
action={
<div>
<IconButton aria-label='Copy URL'>
<FileCopyIcon
onClick={() => copy(createURL(window.location.href, config ? config.urls.route : '/go', u.vanity || u.id))}
/>
</IconButton>
<Link href={u.url}>
<a target='_blank'>
<IconButton
aria-label='Open URL in new Tab'
>
<OpenInNewIcon />
</IconButton>
</a>
</Link>
<IconButton
aria-label='Delete Forever'
onClick={() => deleteUrl(u)}
>
<DeleteForeverIcon />
</IconButton>
</div>
}
title={u.vanity ? u.vanity : u.id}
/>
</Card>
</Grid>
))) : (
<Grid
container
spacing={0}

View File

@@ -219,7 +219,7 @@ export default function Users() {
}
title={`${u.username} (${u.id})`}
subheader={`${u.administrator ? 'Administrator' : 'User'
}`}
}`}
/>
</Card>
</Grid>

View File

@@ -10,8 +10,7 @@ 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>

View File

@@ -0,0 +1,67 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { Config } from '../lib/Config';
import { Plugin } from '../lib/plugin';
import { URL } from '../lib/entities/URL';
import Server from 'next/dist/next-server/server/next-server';
import { Connection } from 'typeorm';
import { existsSync } from 'fs';
import { join } from 'path';
export default class implements Plugin {
public name: string = "assets";
public onLoad(server: FastifyInstance, orm: Connection, app: Server, config: Config) {
server.get(`${config.urls.route}/:id`, async function (
req: FastifyRequest<{ Params: { id: string } }>,
reply: FastifyReply
) {
const urls = orm.getRepository(URL);
const urlId = await urls.findOne({
where: {
id: req.params.id
}
});
const urlVanity = await urls.findOne({
where: {
vanity: req.params.id
}
});
if (config.urls.vanity && urlVanity) return reply.redirect(urlVanity.url);
if (!urlId) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
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>
`);
});
}
}

View File

@@ -0,0 +1,34 @@
import { FastifyInstance } from 'fastify';
import { Config } from '../lib/Config';
import { Plugin } from '../lib/plugin';
import { Console } from '../lib/logger';
import Server from 'next/dist/next-server/server/next-server';
import { Connection } from 'typeorm';
import { bold, green, red } from '@dicedtomato/colors';
export default class implements Plugin {
public name: string = "fastify_hooks";
public onLoad(server: FastifyInstance, orm: Connection, app: Server, config: Config) {
server.addHook('preHandler', async (req, reply) => {
if (
config.core.blacklisted_ips &&
config.core.blacklisted_ips.includes(req.ip)
) {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
}
});
server.addHook('onResponse', (req, res, done) => {
if (!req.url.startsWith('/_next') && config.core.log) {
const status =
res.statusCode !== 200
? bold(red(res.statusCode.toString()))
: bold(green(res.statusCode.toString()));
Console.logger('server').info(`${status} ${req.url} was accessed`);
}
done();
});
}
}

View File

@@ -7,17 +7,20 @@ export const UPDATE_USER = 'UPDATE_USER';
export const STOP_LOADING = 'STOP_LOADING';
export const START_LOADING = 'START_LOADING';
export const SET_THEME = 'SET_THEME';
export type Theme = 'dark-dark' | 'light' | 'blue-dark';
export interface State {
loggedIn: boolean;
user: User;
loading: boolean;
theme: Theme;
}
const initialState: State = {
loggedIn: false,
user: null,
loading: true
loading: true,
theme: 'dark-dark'
};
export function reducer(state: State = initialState, action) {
@@ -28,6 +31,8 @@ export function reducer(state: State = initialState, action) {
return { ...state, loggedIn: false };
case UPDATE_USER:
return { ...state, user: action.payload };
case SET_THEME:
return { ...state, theme: action.payload };
case START_LOADING:
return { ...state, loading: true };
case STOP_LOADING:

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"outDir": "./dist",
"allowJs": true,
"skipLibCheck": true,
@@ -16,6 +20,12 @@
"experimentalDecorators": true,
"noEmit": false
},
"include": ["next-env.d.ts", "src"],
"exclude": ["node_modules", ".next"]
}
"include": [
"next-env.d.ts",
"src"
],
"exclude": [
"node_modules",
".next"
]
}

3646
yarn.lock

File diff suppressed because it is too large Load Diff