Compare commits

...

5 Commits
2.5.7 ... 2.6.0

Author SHA1 Message Date
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
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
14 changed files with 362 additions and 207 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "zipline-next",
"version": "2.5.7",
"version": "2.6.0",
"private": true,
"dependencies": {
"@dicedtomato/colors": "^1.0.3",

View File

@@ -1,37 +1,25 @@
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/controllers/UserController';
import { RootController } from './lib/controllers/RootController';
import { join } from 'path';
import { ImagesController } from './lib/controllers/ImagesController';
import { URLSController } from './lib/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/controllers/MultiFactorController';
import { PluginLoader } from './lib/plugin';
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!'
);
})();
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();
@@ -42,188 +30,71 @@ if (!config) {
process.exit(0);
}
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);
}
if (config.core.log) 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'))}
`);
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
(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 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>
`);
});
const dir = config.uploader.directory ? config.uploader.directory : 'uploads';
const path = dir.charAt(0) == '/' ? dir : join(process.cwd(), dir);
const handle = app.getRequestHandler();
server.register(fastifyMultipart);
if (dev) server.get('/_next/*', async (req, reply) => {
await handle(req.raw, reply.raw);
return (reply.sent = true);
});
server.register(fastifyTypeorm, {
...config.database,
entities: [Image, URL, User, Zipline],
synchronize: true,
logging: false
});
server.all('/*', async (req, reply) => {
await handle(req.raw, reply.raw);
return (reply.sent = true);
});
server.register(bootstrap, {
controllers: [
UserController,
RootController,
ImagesController,
URLSController,
MultiFactorController
]
});
server.setNotFoundHandler(async (req, reply) => {
await app.render404(req.raw, reply.raw);
return (reply.sent = true);
});
server.register(fastifyCookies, {
secret: config.core.secret
});
server.register(fastifyStatic, {
root: path,
prefix: config.uploader.route
});
server.register(fastifyStatic, {
root: path,
prefix: config.uploader.route
});
server.register(fastifyStatic, {
root: join(process.cwd(), 'public'),
prefix: '/public',
decorateReply: false
});
// 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}`)
}
}
})
server.register(fastifyFavicon);
server.listen(
{
port: config.core.port,
host: config.core.host
},
async err => {
if (err) throw err;
const info = server.server.address() as AddressInfo;
server.listen(
{
port: config.core.port,
host: config.core.host
},
err => {
if (err) throw err;
const info = server.server.address() as AddressInfo;
Console.logger('Server').info(
`server listening on ${bold(
`${green(info.address)}${reset(':')}${bold(
green(info.port.toString())
Console.logger('Server').info(
`server listening on ${bold(
`${green(info.address)}${reset(':')}${bold(
green(info.port.toString())
)}`
)}`
)}`
);
}
);
server.addHook('preHandler', async (req, reply) => {
if (
config.core.blacklisted_ips &&
config.core.blacklisted_ips.includes(req.ip)
) {
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

@@ -127,4 +127,30 @@ export class MultiFactorController {
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

@@ -128,7 +128,7 @@ export class RootController {
if (config.uploader.blacklisted.includes(ext)) return sendError(reply, 'Blacklisted file extension!');
const fileName = config.uploader.original
? data.filename.split('.')[0]
? data.filename.split('.').pop()
: createRandomId(config.uploader.length);
const path = join(config.uploader.directory, `${fileName}.${ext}`);

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: boolean = 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' : 'src', 'lib', 'plugin', 'builtins') : this.directory);
return result;
}
public async loadPlugins(builtIn: boolean = 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

@@ -34,7 +34,7 @@ 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 [stats, setStats] = React.useState<{ totalViews: number, averageViews: number, images: number }>(null);
const [recentImages, setRecentImages] = React.useState([]);
if (typeof window === 'undefined') return <UIPlaceholder />;
@@ -61,7 +61,7 @@ export default function Dashboard({ config }) {
{!loading ? (
<Paper elevation={3} className={classes.padding}>
<Typography variant='h4'>
Welcome back, {state.user.username}
Welcome back, {state.user.username}
</Typography>
<Typography color='textSecondary'>
You have <b>{stats.images}</b> images, with <b>{stats.totalViews}</b> ({Math.round(stats.averageViews)}) collectively.

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

@@ -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,13 @@
"experimentalDecorators": true,
"noEmit": false
},
"include": ["next-env.d.ts", "src"],
"exclude": ["node_modules", ".next"]
}
"include": [
"next-env.d.ts",
"src",
"Zipline.toml"
],
"exclude": [
"node_modules",
".next"
]
}