mirror of
https://github.com/diced/zipline.git
synced 2025-12-19 10:42:40 -08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ed55cae21 | ||
|
|
c3027b9369 | ||
|
|
819fd1a2b1 | ||
|
|
b5d64b0798 | ||
|
|
dc01caeab0 | ||
|
|
df646f7433 | ||
|
|
4239b610fc | ||
|
|
44640b7d26 | ||
|
|
1745298f47 | ||
|
|
0bd83f731f | ||
|
|
749ca44ac5 | ||
|
|
2857bae6be | ||
|
|
a72d1e1d66 | ||
|
|
f45e06ef9e | ||
|
|
3bf154c6d0 | ||
|
|
bc9fa4e063 | ||
|
|
6ab39fb94d | ||
|
|
e520f1e589 | ||
|
|
f008a492a3 | ||
|
|
eff47404df | ||
|
|
844dd2d4ed | ||
|
|
2ab17aa297 | ||
|
|
776b0aa3c4 | ||
|
|
701e5ae2d0 | ||
|
|
1529eb3afd | ||
|
|
d3676c2662 | ||
|
|
cb93df347c | ||
|
|
c31a2172eb | ||
|
|
e3d0f5e47d | ||
|
|
975fc00fad | ||
|
|
03475bd7d7 | ||
|
|
6992b0eb67 | ||
|
|
c796927b35 | ||
|
|
00eb6aef41 | ||
|
|
39f2773703 | ||
|
|
59ce5e5cce | ||
|
|
a0360269b8 | ||
|
|
6270c725dc | ||
|
|
b82a50ae4e |
21
.github/workflows/docker-hub.yml
vendored
Normal file
21
.github/workflows/docker-hub.yml
vendored
Normal 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
22
.github/workflows/docker.yml
vendored
Normal 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
|
||||||
16
README.md
16
README.md
@@ -12,27 +12,21 @@
|
|||||||

|

|
||||||
|
|
||||||
# Zipline
|
# Zipline
|
||||||
The best and only **React + Next.js** ShareX / File Uploader you would ever want.
|
|
||||||
|
|
||||||
# Comparison
|
The best **React/Next.js** ShareX / File Uploader you would ever want.
|
||||||
Wondering how Zipline compares to other popular uploaders? We have done some benchmarking on other popular upload servers, see how Zipline compares.
|
|
||||||
|
|
||||||
| Uploader | Average ms (3 batches/1.5k files) |
|
# [Comparison between other uploaders](https://zipline.diced.wtf/docs/comparison)
|
||||||
|-|-|
|
|
||||||
| **[Zipline](https://github.com/diced/zipline)** | **61 ms** |
|
|
||||||
| [ShareX-Upload-Server](https://github.com/TannerReynolds/ShareX-Upload-Server) | 86 ms |
|
|
||||||
|
|
||||||
*Note: there were 3 batches of 1.5k requests, the average ms of each was averaged again*<br>
|
|
||||||
*Note 2: results will vary because its very dependent on the server, location, and your internet (these tests were run on the same machine with local dbs)*
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Configurable
|
- Configurable
|
||||||
- Fast (API)
|
- Fast (API)
|
||||||
- Built with Next.js & React
|
- 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
|
- Token protected uploading
|
||||||
- MFA with Authy/Google Authenticator
|
- MFA with Authy/Google Authenticator
|
||||||
- Easy setup instructions on [docs](https://zipline.diced.wtf/docs)
|
- Easy setup instructions on [docs](https://zipline.diced.wtf/docs)
|
||||||
|
|
||||||
# Installing
|
# Installing
|
||||||
|
|
||||||
[See how to install here](https://zipline.diced.wtf/docs/)
|
[See how to install here](https://zipline.diced.wtf/docs/)
|
||||||
|
|||||||
9
docker-compose.template.yml
Normal file
9
docker-compose.template.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
zipline:
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- "./uploads:/opt/zipline/uploads"
|
||||||
|
build: .
|
||||||
|
tty: true
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zipline-next",
|
"name": "zipline-next",
|
||||||
"version": "2.5.7",
|
"version": "2.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dicedtomato/colors": "^1.0.3",
|
"@dicedtomato/colors": "^1.0.3",
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
"dev:verbose": "VERBOSE=true ts-node src",
|
"dev:verbose": "VERBOSE=true ts-node src",
|
||||||
"build": "next build && tsc -p .",
|
"build": "next build && tsc -p .",
|
||||||
"start": "NODE_ENV=production node dist",
|
"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"
|
"start:verbose": "NODE_ENV=production VERBOSE=true node dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
11
setup.js
11
setup.js
@@ -48,14 +48,14 @@ const base = {
|
|||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
name: 'type',
|
name: 'type',
|
||||||
message: 'What database type?',
|
message: 'What database type? (you will have to install the drivers)',
|
||||||
choices: [
|
choices: [
|
||||||
{ name: 'postgres', extra: 'This is what we recomend using.' },
|
{ name: 'postgres', extra: 'This is what we recomend using.' },
|
||||||
{ name: 'cockroachdb' },
|
{ name: 'cockroachdb' },
|
||||||
{ name: 'mysql' },
|
{ name: 'mysql' },
|
||||||
{ name: 'mariadb' },
|
{ name: 'mariadb' },
|
||||||
{ name: 'mssql' },
|
{ name: 'mssql' },
|
||||||
{ name: 'sqlite3' }
|
{ name: 'sqlite' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -103,14 +103,14 @@ const base = {
|
|||||||
name: 'theme',
|
name: 'theme',
|
||||||
message: 'Theme',
|
message: 'Theme',
|
||||||
choices: [
|
choices: [
|
||||||
{ name: 'Dark Theme (recomended)' },
|
{ name: 'dark' },
|
||||||
{ name: 'Light Theme (warning for eyes)' }
|
{ name: 'light' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'mfa',
|
name: 'mfa',
|
||||||
message: 'Enable MFA with Authy/Google Authenticator'
|
message: 'Enable 2 Factor Authentication with Authy/Google Authenticator'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ const base = {
|
|||||||
'Head to https://zipline.diced.wtf/docs/docker to learn how to run with docker.'
|
'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));
|
writeFileSync('Zipline.toml', stringify(config));
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Typography from '@material-ui/core/Typography';
|
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 CardContent from '@material-ui/core/CardContent';
|
||||||
import CardActions from '@material-ui/core/CardActions';
|
import CardActions from '@material-ui/core/CardActions';
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
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 TextField from '@material-ui/core/TextField';
|
||||||
import Snackbar from '@material-ui/core/Snackbar';
|
import Snackbar from '@material-ui/core/Snackbar';
|
||||||
import Grid from '@material-ui/core/Grid';
|
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 Alert from '@material-ui/lab/Alert';
|
||||||
import { makeStyles } from '@material-ui/core';
|
import { makeStyles } from '@material-ui/core';
|
||||||
import { UPDATE_USER } from '../reducer';
|
import { SET_THEME, UPDATE_USER } from '../reducer';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Config } from '../lib/Config';
|
import { Config } from '../lib/Config';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
margin: {
|
margin: {
|
||||||
@@ -37,7 +40,12 @@ const useStyles = makeStyles({
|
|||||||
export default function ManageUser({ config }: { config: Config }) {
|
export default function ManageUser({ config }: { config: Config }) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
const [theme, setTheme] = useState(
|
||||||
|
state.theme === '' ? 'dark-dark' : state.theme
|
||||||
|
);
|
||||||
const [alertOpen, setAlertOpen] = useState(false);
|
const [alertOpen, setAlertOpen] = useState(false);
|
||||||
const [mfaDialogOpen, setMfaDialogOpen] = useState(false);
|
const [mfaDialogOpen, setMfaDialogOpen] = useState(false);
|
||||||
const [qrcode, setQRCode] = useState(null);
|
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 () => {
|
const disableMFA = async () => {
|
||||||
await fetch('/api/mfa/disable');
|
await fetch('/api/mfa/disable');
|
||||||
const d = await (await fetch('/api/user')).json();
|
const d = await (await fetch('/api/user')).json();
|
||||||
@@ -157,11 +171,21 @@ export default function ManageUser({ config }: { config: Config }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Card>
|
<Paper>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography color='textSecondary' variant='h4' gutterBottom>
|
<Typography color='textSecondary' variant='h4' gutterBottom>
|
||||||
Manage
|
Manage
|
||||||
</Typography>
|
</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
|
<TextField
|
||||||
label='Username'
|
label='Username'
|
||||||
className={classes.field}
|
className={classes.field}
|
||||||
@@ -186,6 +210,13 @@ export default function ManageUser({ config }: { config: Config }) {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
|
<Button
|
||||||
|
className={classes.button}
|
||||||
|
color='primary'
|
||||||
|
onClick={handleUpdateTheme}
|
||||||
|
>
|
||||||
|
settheme
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={classes.button}
|
className={classes.button}
|
||||||
color='primary'
|
color='primary'
|
||||||
@@ -203,7 +234,7 @@ export default function ManageUser({ config }: { config: Config }) {
|
|||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Paper>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
appBar: {
|
appBar: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#fff',
|
backgroundColor: theme.palette.background.default,
|
||||||
color: theme.palette.type !== 'dark' ? '#000' : '#fff',
|
color: theme.palette.text.primary,
|
||||||
|
...theme.overrides.MuiAppBar.root,
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up('sm')]: {
|
||||||
width: 'calc(100%)',
|
width: 'calc(100%)',
|
||||||
marginLeft: drawerWidth
|
marginLeft: drawerWidth
|
||||||
},
|
}
|
||||||
borderBottom: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0'
|
|
||||||
},
|
},
|
||||||
menuButton: {
|
menuButton: {
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
@@ -188,7 +188,9 @@ export default function UI({ children }) {
|
|||||||
color='inherit'
|
color='inherit'
|
||||||
className={classes.rightButton}
|
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()}
|
{state.user.username[0].toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -222,9 +224,7 @@ export default function UI({ children }) {
|
|||||||
onClose={() => setAnchorEl(null)}
|
onClose={() => setAnchorEl(null)}
|
||||||
>
|
>
|
||||||
<NoFocusMenuItem>
|
<NoFocusMenuItem>
|
||||||
<Typography variant='h6'>
|
<Typography variant='h6'>{state.user.username}</Typography>
|
||||||
{state.user.username}
|
|
||||||
</Typography>
|
|
||||||
</NoFocusMenuItem>
|
</NoFocusMenuItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Link href='/user/manage'>
|
<Link href='/user/manage'>
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import { ThemeProvider } from '@material-ui/core/styles';
|
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 light from '../lib/themes/light';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function ZiplineTheming({ Component, pageProps, theme }) {
|
export default function ZiplineTheming({ Component, pageProps, theme }) {
|
||||||
|
const thm = {
|
||||||
|
'dark-dark': darkdark,
|
||||||
|
light: light,
|
||||||
|
'blue-dark': bluedark,
|
||||||
|
'': darkdark
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme == 'light' ? light : dark}>
|
<ThemeProvider theme={thm[theme]}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
271
src/index.ts
271
src/index.ts
@@ -1,37 +1,26 @@
|
|||||||
import next from 'next';
|
import next from 'next';
|
||||||
import { textSync as text } from 'figlet';
|
import fastify from 'fastify';
|
||||||
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 fastifyStatic from 'fastify-static';
|
import fastifyStatic from 'fastify-static';
|
||||||
import fastifyFavicon from 'fastify-favicon';
|
import fastifyTypeorm from 'fastify-typeorm-plugin';
|
||||||
import { bootstrap } from 'fastify-decorators';
|
|
||||||
import { Console } from './lib/logger';
|
import { Console } from './lib/logger';
|
||||||
import { AddressInfo } from 'net';
|
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 { Configuration } from './lib/Config';
|
||||||
import { UserController } from './lib/controllers/UserController';
|
|
||||||
import { RootController } from './lib/controllers/RootController';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { ImagesController } from './lib/controllers/ImagesController';
|
|
||||||
import { URLSController } from './lib/controllers/URLSController';
|
|
||||||
import { checkVersion } from './lib/Util';
|
import { checkVersion } from './lib/Util';
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { PluginLoader } from './lib/plugin';
|
||||||
import { Image } from './lib/entities/Image';
|
import { readdirSync, statSync } from 'fs';
|
||||||
import { User } from './lib/entities/User';
|
|
||||||
import { Zipline } from './lib/entities/Zipline';
|
|
||||||
import { URL } from './lib/entities/URL';
|
|
||||||
import { MultiFactorController } from './lib/controllers/MultiFactorController';
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
|
const server = fastify({});
|
||||||
|
const app = next({
|
||||||
|
dev,
|
||||||
|
quiet: dev
|
||||||
|
});
|
||||||
|
|
||||||
(async () => {
|
app.prepare();
|
||||||
if (await checkVersion()) Console.logger('Zipline').info(
|
|
||||||
'running an outdated version of zipline, please update soon!'
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
const pluginLoader = new PluginLoader(server, process.cwd(), dev ? './src/plugins' : './dist/plugins');
|
||||||
Console.logger(Configuration).verbose('searching for config...');
|
Console.logger(Configuration).verbose('searching for config...');
|
||||||
const config = Configuration.readConfig();
|
const config = Configuration.readConfig();
|
||||||
|
|
||||||
@@ -42,188 +31,72 @@ if (!config) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.core || !config.database) {
|
(async () => {
|
||||||
Console.logger('Zipline').error(
|
const builtInPlugins = await pluginLoader.loadPlugins(true);
|
||||||
'configuration seems to be invalid, did you generate a config? https://zipline.diced.wtf/docs/auto'
|
for (const plugin of builtInPlugins) {
|
||||||
);
|
try {
|
||||||
process.exit(0);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.core.log) console.log(`
|
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
|
||||||
${magenta(text('Zipline'))}
|
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
|
||||||
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
Version : ${blue(
|
server.get('/*', async (req, reply) => {
|
||||||
process.env.npm_package_version ||
|
const routeRegex = /\/_next\/static|\/((dash|user)(\/)?(.+)?)?/gi;
|
||||||
JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'))
|
if (routeRegex.test(req.url)) {
|
||||||
.version
|
await handle(req.raw, reply.raw);
|
||||||
)}
|
return (reply.sent = true);
|
||||||
GitHub : ${blue('https://github.com/ZiplineProject/zipline')}
|
} else {
|
||||||
Issues : ${blue('https://github.com/ZiplineProject/zipline/issues')}
|
await app.render404(req.raw, reply.raw);
|
||||||
Docs : ${blue('https://zipline.diced.wtf/')}
|
return (reply.sent = true);
|
||||||
Mode : ${bold(dev ? red('dev') : green('production'))}
|
|
||||||
Verbose : ${bold(process.env.VERBOSE ? red('yes') : green('no'))}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
|
|
||||||
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
|
|
||||||
|
|
||||||
const server = fastify({});
|
|
||||||
const app = next({
|
|
||||||
dev,
|
|
||||||
quiet: dev
|
|
||||||
});
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const urlVanity = await urls.findOne({
|
server.setNotFoundHandler(async (req, reply) => {
|
||||||
where: {
|
await app.render404(req.raw, reply.raw);
|
||||||
vanity: req.params.id
|
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);
|
server.listen(
|
||||||
if (!urlId) {
|
{
|
||||||
await app.render404(req.raw, reply.raw);
|
port: config.core.port,
|
||||||
return (reply.sent = true);
|
host: config.core.host
|
||||||
}
|
},
|
||||||
return reply.redirect(urlId.url);
|
async err => {
|
||||||
});
|
if (err) throw err;
|
||||||
|
const info = server.server.address() as AddressInfo;
|
||||||
|
|
||||||
server.get(`${config.uploader.rich_content_route || '/a'}/:id`, async function (
|
Console.logger('Server').info(
|
||||||
req: FastifyRequest<{ Params: { id: string } }>,
|
`server listening on ${bold(
|
||||||
reply: FastifyReply
|
`${green(info.address)}${reset(':')}${bold(
|
||||||
) {
|
green(info.port.toString())
|
||||||
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())
|
|
||||||
)}`
|
)}`
|
||||||
)}`
|
);
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
);
|
})();
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
@@ -59,7 +59,7 @@ export class Webhooks {
|
|||||||
public static parseContent(content: string, data: WebhookData) {
|
public static parseContent(content: string, data: WebhookData) {
|
||||||
return content
|
return content
|
||||||
.replace(WebhookParseTokens.IMAGE_ID, data.image?.id)
|
.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_ID, data.url?.id)
|
||||||
.replace(WebhookParseTokens.URL_URL, data.host + data.url?.id)
|
.replace(WebhookParseTokens.URL_URL, data.host + data.url?.id)
|
||||||
.replace(WebhookParseTokens.URL_VANITY, data.url?.vanity)
|
.replace(WebhookParseTokens.URL_VANITY, data.url?.vanity)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export class ImagesController {
|
|||||||
if (!f.length) return array;
|
if (!f.length) return array;
|
||||||
return [f].concat(chunk(array.slice(size, array.length), size));
|
return [f].concat(chunk(array.slice(size, array.length), size));
|
||||||
}
|
}
|
||||||
const chunks = chunk(images, 20);
|
const chunks = chunk(images, 26);
|
||||||
return reply.send(chunks);
|
return reply.send(chunks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,4 +127,30 @@ export class MultiFactorController {
|
|||||||
|
|
||||||
return reply.send({ user, passed });
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,17 +123,18 @@ export class RootController {
|
|||||||
const data: Multipart = await req.file();
|
const data: Multipart = await req.file();
|
||||||
|
|
||||||
if (!existsSync(config.uploader.directory)) mkdirSync(config.uploader.directory);
|
if (!existsSync(config.uploader.directory)) mkdirSync(config.uploader.directory);
|
||||||
|
|
||||||
const ext = data.mimetype === 'application/octet-stream' ? 'bin' : data.filename.split('.')[1];
|
const og = data.filename;
|
||||||
|
const ext = data.filename.split('.').pop();
|
||||||
if (config.uploader.blacklisted.includes(ext)) return sendError(reply, 'Blacklisted file extension!');
|
if (config.uploader.blacklisted.includes(ext)) return sendError(reply, 'Blacklisted file extension!');
|
||||||
|
console.log(data.filename);
|
||||||
const fileName = config.uploader.original
|
const fileName = config.uploader.original
|
||||||
? data.filename.split('.')[0]
|
? og
|
||||||
: createRandomId(config.uploader.length);
|
: 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`);
|
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(`saved image ${image.id} to db`);
|
||||||
|
|
||||||
this.logger.verbose(`attempting to save file ${path}`);
|
this.logger.verbose(`attempting to save file ${path}`);
|
||||||
@@ -148,10 +149,11 @@ export class RootController {
|
|||||||
config.uploader.rich_content_route
|
config.uploader.rich_content_route
|
||||||
? config.uploader.rich_content_route
|
? config.uploader.rich_content_route
|
||||||
: config.uploader.route
|
: config.uploader.route
|
||||||
}/${fileName}.${ext}`;
|
}/${config.uploader.original ? og : `${fileName}.${ext}`}`;
|
||||||
|
|
||||||
if (this.webhooks.events.includes(WebhookType.UPLOAD)) Webhooks.sendWebhook(this.webhooks.upload.content, {
|
if (this.webhooks.events.includes(WebhookType.UPLOAD)) Webhooks.sendWebhook(this.webhooks.upload.content, {
|
||||||
image,
|
image,
|
||||||
|
user,
|
||||||
host
|
host
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,8 @@ export class UserController {
|
|||||||
this.logger.verbose(`set cookie for ${user.username} (${user.id})`);
|
this.logger.verbose(`set cookie for ${user.username} (${user.id})`);
|
||||||
reply.setCookie('zipline', createBaseCookie(user.id), {
|
reply.setCookie('zipline', createBaseCookie(user.id), {
|
||||||
path: '/',
|
path: '/',
|
||||||
maxAge: 1036800000
|
maxAge: 1036800000,
|
||||||
|
signed: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info(`${user.username} (${user.id}) logged in`);
|
this.logger.info(`${user.username} (${user.id}) logged in`);
|
||||||
@@ -193,7 +194,6 @@ export class UserController {
|
|||||||
|
|
||||||
@POST('/reset-token')
|
@POST('/reset-token')
|
||||||
async resetToken(req: FastifyRequest, reply: FastifyReply) {
|
async resetToken(req: FastifyRequest, reply: FastifyReply) {
|
||||||
if (!req.cookies.zipline) return sendError(reply, 'Not logged in.');
|
|
||||||
|
|
||||||
const user = await this.users.findOne({
|
const user = await this.users.findOne({
|
||||||
where: {
|
where: {
|
||||||
@@ -224,6 +224,10 @@ export class UserController {
|
|||||||
}>,
|
}>,
|
||||||
reply: FastifyReply
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
|
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.username) return sendError(reply, 'Missing username.');
|
||||||
if (!req.body.password) return sendError(reply, 'Missing uassword.');
|
if (!req.body.password) return sendError(reply, 'Missing uassword.');
|
||||||
|
|
||||||
@@ -232,6 +236,8 @@ export class UserController {
|
|||||||
});
|
});
|
||||||
if (existing) return sendError(reply, 'User exists already');
|
if (existing) return sendError(reply, 'User exists already');
|
||||||
|
|
||||||
|
if (req.body.username.length > 25) return sendError(reply, 'Limit 25');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.verbose(`attempting to create ${req.body.username}`);
|
this.logger.verbose(`attempting to create ${req.body.username}`);
|
||||||
const user = await this.users.save(
|
const user = await this.users.save(
|
||||||
@@ -247,7 +253,6 @@ export class UserController {
|
|||||||
user
|
user
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstSetup = await getFirst(this.instance.orm);
|
|
||||||
if (firstSetup) await this.instance.orm.getRepository(Zipline).update(
|
if (firstSetup) await this.instance.orm.getRepository(Zipline).update(
|
||||||
{
|
{
|
||||||
id: 'zipline'
|
id: 'zipline'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Entity, Column, PrimaryColumn } from 'typeorm';
|
|||||||
|
|
||||||
@Entity({ name: 'zipline_images' })
|
@Entity({ name: 'zipline_images' })
|
||||||
export class Image {
|
export class Image {
|
||||||
@PrimaryColumn('text')
|
@PrimaryColumn('varchar', { length: 255 })
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column('text', { default: null })
|
@Column('text', { default: null })
|
||||||
@@ -14,9 +14,10 @@ export class Image {
|
|||||||
@Column('bigint', { default: '0' })
|
@Column('bigint', { default: '0' })
|
||||||
public views: number;
|
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.id = id;
|
||||||
this.file = `${id}.${ext}`;
|
if (original) this.file = id;
|
||||||
|
else this.file = `${id}.${ext}`;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.views = 0;
|
this.views = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ConsoleLevel } from '.';
|
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 {
|
export interface Formatter {
|
||||||
format(
|
format(
|
||||||
@@ -27,7 +27,7 @@ export class DefaultFormatter implements Formatter {
|
|||||||
level: ConsoleLevel,
|
level: ConsoleLevel,
|
||||||
time: Date
|
time: Date
|
||||||
): string {
|
): string {
|
||||||
return `[${time.toLocaleString().replace(',', '')}] ${this.formatLevel(
|
return `[${time.toLocaleString().replace(',', '')}] [${green(origin.toLowerCase())}] ${this.formatLevel(
|
||||||
level
|
level
|
||||||
)} ${reset(message)}`;
|
)} ${reset(message)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/lib/plugin/Plugin.ts
Normal file
11
src/lib/plugin/Plugin.ts
Normal 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;
|
||||||
|
}
|
||||||
45
src/lib/plugin/PluginLoader.ts
Normal file
45
src/lib/plugin/PluginLoader.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/lib/plugin/builtins/FastifyPlugin.ts
Normal file
59
src/lib/plugin/builtins/FastifyPlugin.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/lib/plugin/builtins/LogPlugin.ts
Normal file
29
src/lib/plugin/builtins/LogPlugin.ts
Normal 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
2
src/lib/plugin/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './Plugin';
|
||||||
|
export * from './PluginLoader';
|
||||||
49
src/lib/themes/bluedark.ts
Normal file
49
src/lib/themes/bluedark.ts
Normal 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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
|
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
|
||||||
|
|
||||||
const darkTheme = createMuiTheme({
|
const darkdarkTheme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: 'dark',
|
type: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
@@ -22,6 +22,12 @@ const darkTheme = createMuiTheme({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
root: {
|
||||||
|
borderBottom: '#1f1f1f',
|
||||||
|
backgroundColor: '#000000'
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiCard: {
|
MuiCard: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: '#080808'
|
backgroundColor: '#080808'
|
||||||
@@ -35,4 +41,4 @@ const darkTheme = createMuiTheme({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default darkTheme;
|
export default darkdarkTheme;
|
||||||
@@ -22,6 +22,11 @@ const lightTheme = createMuiTheme({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
MuiAppBar: {
|
||||||
|
root: {
|
||||||
|
borderBottom: '#1f1f1f'
|
||||||
|
}
|
||||||
|
},
|
||||||
MuiCard: {
|
MuiCard: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: '#fff'
|
backgroundColor: '#fff'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Typography from '@material-ui/core/Typography';
|
|||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import Grid from '@material-ui/core/Grid';
|
import Grid from '@material-ui/core/Grid';
|
||||||
import { ThemeProvider } from '@material-ui/core/styles';
|
import { ThemeProvider } from '@material-ui/core/styles';
|
||||||
import dark from '../lib/themes/dark';
|
import dark from '../lib/themes/darkdark';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@@ -8,15 +8,11 @@ import ZiplineTheming from '../components/ZiplineTheming';
|
|||||||
import UIPlaceholder from '../components/UIPlaceholder';
|
import UIPlaceholder from '../components/UIPlaceholder';
|
||||||
|
|
||||||
function App({ Component, pageProps }) {
|
function App({ Component, pageProps }) {
|
||||||
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
const state = store.getState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const jssStyles = document.querySelector('#jss-server-side');
|
const jssStyles = document.querySelector('#jss-server-side');
|
||||||
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
|
if (jssStyles) jssStyles.parentElement.removeChild(jssStyles);
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const d = await (await fetch('/api/theme')).json();
|
|
||||||
if (!d.error) setTheme(d.theme);
|
|
||||||
})();
|
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -33,7 +29,7 @@ function App({ Component, pageProps }) {
|
|||||||
<ZiplineTheming
|
<ZiplineTheming
|
||||||
Component={Component}
|
Component={Component}
|
||||||
pageProps={pageProps}
|
pageProps={pageProps}
|
||||||
theme={theme}
|
theme={state.theme}
|
||||||
/>
|
/>
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -46,4 +42,4 @@ App.propTypes = {
|
|||||||
pageProps: PropTypes.object.isRequired
|
pageProps: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||||
import { ServerStyleSheets } from '@material-ui/core/styles';
|
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';
|
import { Config, Configuration } from '../lib/Config';
|
||||||
|
|
||||||
export interface DocumentProps {
|
export interface DocumentProps {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const useStyles = makeStyles(theme => ({
|
|||||||
margin: '5px'
|
margin: '5px'
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
border: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0',
|
|
||||||
padding: '10px'
|
padding: '10px'
|
||||||
},
|
},
|
||||||
backdrop: {
|
backdrop: {
|
||||||
@@ -34,7 +33,11 @@ export default function Dashboard({ config }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [stats, setStats] = React.useState<{totalViews:number, averageViews:number, images: number}>(null);
|
const [stats, setStats] = React.useState<{
|
||||||
|
totalViews: number;
|
||||||
|
averageViews: number;
|
||||||
|
images: number;
|
||||||
|
}>(null);
|
||||||
const [recentImages, setRecentImages] = React.useState([]);
|
const [recentImages, setRecentImages] = React.useState([]);
|
||||||
|
|
||||||
if (typeof window === 'undefined') return <UIPlaceholder />;
|
if (typeof window === 'undefined') return <UIPlaceholder />;
|
||||||
@@ -61,10 +64,12 @@ export default function Dashboard({ config }) {
|
|||||||
{!loading ? (
|
{!loading ? (
|
||||||
<Paper elevation={3} className={classes.padding}>
|
<Paper elevation={3} className={classes.padding}>
|
||||||
<Typography variant='h4'>
|
<Typography variant='h4'>
|
||||||
Welcome back, {state.user.username}
|
Welcome back, {state.user.username}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color='textSecondary'>
|
<Typography color='textSecondary'>
|
||||||
You have <b>{stats.images}</b> images, with <b>{stats.totalViews}</b> ({Math.round(stats.averageViews)}) collectively.
|
You have <b>{stats.images}</b> images, with{' '}
|
||||||
|
<b>{stats.totalViews}</b> ({Math.round(stats.averageViews)})
|
||||||
|
collectively.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='h5'>Recent Images</Typography>
|
<Typography variant='h5'>Recent Images</Typography>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@@ -76,7 +81,11 @@ export default function Dashboard({ config }) {
|
|||||||
<CardMedia
|
<CardMedia
|
||||||
component='img'
|
component='img'
|
||||||
height='140'
|
height='140'
|
||||||
image={createURL(window.location.href, config ? config.uploader.route : '/u', d.file)}
|
image={createURL(
|
||||||
|
window.location.href,
|
||||||
|
config ? config.uploader.route : '/u',
|
||||||
|
d.file
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -95,4 +104,4 @@ export default function Dashboard({ config }) {
|
|||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const config = Configuration.readConfig();
|
const config = Configuration.readConfig();
|
||||||
return { props: { config: config } };
|
return { props: { config: config } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ const useStyles = makeStyles(theme => ({
|
|||||||
margin: '5px'
|
margin: '5px'
|
||||||
},
|
},
|
||||||
padding: {
|
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'
|
padding: '10px'
|
||||||
},
|
},
|
||||||
backdrop: {
|
backdrop: {
|
||||||
@@ -57,6 +58,7 @@ export default function Images({ config }) {
|
|||||||
const c = await (await fetch('/api/images/chunk')).json();
|
const c = await (await fetch('/api/images/chunk')).json();
|
||||||
if (!c.error) {
|
if (!c.error) {
|
||||||
setChunks(c);
|
setChunks(c);
|
||||||
|
console.log(c);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -110,7 +112,7 @@ export default function Images({ config }) {
|
|||||||
{showPagination ? (
|
{showPagination ? (
|
||||||
<>
|
<>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{images.map(d => ((
|
{images.map(d => (
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
xs={12}
|
xs={12}
|
||||||
@@ -123,12 +125,16 @@ export default function Images({ config }) {
|
|||||||
<CardMedia
|
<CardMedia
|
||||||
component='img'
|
component='img'
|
||||||
height='140'
|
height='140'
|
||||||
image={createURL(window.location.href, config ? config.uploader.route : '/u', d.file)}
|
image={createURL(
|
||||||
|
window.location.href,
|
||||||
|
config ? config.uploader.route : '/u',
|
||||||
|
d.file
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
)))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Pagination count={chunks.length} onChange={changePage} />
|
<Pagination count={chunks.length} onChange={changePage} />
|
||||||
</>
|
</>
|
||||||
@@ -140,7 +146,7 @@ export default function Images({ config }) {
|
|||||||
alignItems='center'
|
alignItems='center'
|
||||||
justify='center'
|
justify='center'
|
||||||
>
|
>
|
||||||
<Grid item xs={6} sm={12}>
|
<Grid item xs>
|
||||||
<AddToPhotosIcon style={{ fontSize: 100 }} />
|
<AddToPhotosIcon style={{ fontSize: 100 }} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -179,4 +185,4 @@ export default function Images({ config }) {
|
|||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const config = Configuration.readConfig();
|
const config = Configuration.readConfig();
|
||||||
return { props: { config: config } };
|
return { props: { config: config } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function Upload() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Box m={1}>
|
<Box m={1}>
|
||||||
<DropzoneArea
|
<DropzoneArea
|
||||||
acceptedFiles={['image/*']}
|
acceptedFiles={['image/*', 'video/*']}
|
||||||
dropzoneText={'Drag an image or click to upload an image.'}
|
dropzoneText={'Drag an image or click to upload an image.'}
|
||||||
onChange={f => setFiles(f)}
|
onChange={f => setFiles(f)}
|
||||||
filesLimit={1}
|
filesLimit={1}
|
||||||
@@ -106,4 +106,4 @@ export default function Upload() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <UIPlaceholder />;
|
return <UIPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/plugins/AssetsPlugin.ts
Normal file
67
src/plugins/AssetsPlugin.ts
Normal 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>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/plugins/HooksPlugin.ts
Normal file
34
src/plugins/HooksPlugin.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,17 +7,20 @@ export const UPDATE_USER = 'UPDATE_USER';
|
|||||||
export const STOP_LOADING = 'STOP_LOADING';
|
export const STOP_LOADING = 'STOP_LOADING';
|
||||||
export const START_LOADING = 'START_LOADING';
|
export const START_LOADING = 'START_LOADING';
|
||||||
export const SET_THEME = 'SET_THEME';
|
export const SET_THEME = 'SET_THEME';
|
||||||
|
export type Theme = 'dark-dark' | 'light' | 'blue-dark';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
user: null,
|
user: null,
|
||||||
loading: true
|
loading: true,
|
||||||
|
theme: 'dark-dark'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function reducer(state: State = initialState, action) {
|
export function reducer(state: State = initialState, action) {
|
||||||
@@ -28,6 +31,8 @@ export function reducer(state: State = initialState, action) {
|
|||||||
return { ...state, loggedIn: false };
|
return { ...state, loggedIn: false };
|
||||||
case UPDATE_USER:
|
case UPDATE_USER:
|
||||||
return { ...state, user: action.payload };
|
return { ...state, user: action.payload };
|
||||||
|
case SET_THEME:
|
||||||
|
return { ...state, theme: action.payload };
|
||||||
case START_LOADING:
|
case START_LOADING:
|
||||||
return { ...state, loading: true };
|
return { ...state, loading: true };
|
||||||
case STOP_LOADING:
|
case STOP_LOADING:
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -16,6 +20,12 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"noEmit": false
|
"noEmit": false
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "src"],
|
"include": [
|
||||||
"exclude": ["node_modules", ".next"]
|
"next-env.d.ts",
|
||||||
}
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".next"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user