mirror of
https://github.com/diced/zipline.git
synced 2025-12-22 15:16:30 -08:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4152e298 | ||
|
|
307f023e47 | ||
|
|
3451bd8762 | ||
|
|
a9d0be8aae | ||
|
|
d83f720631 | ||
|
|
1f3d396296 | ||
|
|
48f771f344 | ||
|
|
555bc6aa26 | ||
|
|
8bd0eaac1e | ||
|
|
3280c77002 | ||
|
|
b39743a53a | ||
|
|
9a73da56e9 | ||
|
|
c9b0d2664f | ||
|
|
6063c9efac | ||
|
|
dd6f192d4a | ||
|
|
d956f4ed3d |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'quotes': ['error', 'single'],
|
||||
'semi': ['error', 'always'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
@@ -19,6 +20,6 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'@next/next/no-img-element': 'off'
|
||||
}
|
||||
'@next/next/no-img-element': 'off',
|
||||
},
|
||||
};
|
||||
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -23,12 +23,11 @@ jobs:
|
||||
key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Create mock config
|
||||
run: echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
|
||||
|
||||
run: echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-restore.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
run: yarn build
|
||||
1
.github/workflows/docker.yml
vendored
1
.github/workflows/docker.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,6 +18,7 @@
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
.idea
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@@ -35,5 +36,4 @@ yarn-error.log*
|
||||
|
||||
# zipline
|
||||
config.toml
|
||||
uploads/
|
||||
data.db*
|
||||
uploads/
|
||||
@@ -1,6 +1,8 @@
|
||||
FROM node:16-alpine3.11 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
COPY src ./src
|
||||
COPY server ./server
|
||||
COPY scripts ./scripts
|
||||
@@ -11,7 +13,7 @@ COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.j
|
||||
RUN yarn install
|
||||
|
||||
# create a mock config.toml to spoof next build!
|
||||
RUN echo -e "[uploader]\nroute = '/u'\nembed_route = '/a'\nlength = 6\ndirectory = './uploads'" > config.toml
|
||||
RUN echo -e "[core]\nsecret = '12345678'\ndatabase_url = 'postgres://postgres:postgres@postgres/postgres'\n[uploader]\nroute = '/u'\ndirectory = './uploads'\n[urls]\nroute = '/go'" > config.toml
|
||||
|
||||
RUN yarn build
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
prisma
|
||||
node_modules
|
||||
.next
|
||||
uploads
|
||||
.git
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
|
||||
|
||||
Zipline is a file sharing, URL sharing, lightweight and easy to use!
|
||||
Zipline is a ShareX/file upload server that is easy to use, packed with features and can be setup in one command!
|
||||
|
||||

|
||||

|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zip3",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.4",
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"dev": "NODE_ENV=development node server",
|
||||
@@ -9,7 +9,7 @@
|
||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||
"start": "node server",
|
||||
"lint": "next lint",
|
||||
"ts-node": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only",
|
||||
"seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts",
|
||||
"semantic-release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19,7 +19,7 @@
|
||||
"@material-ui/core": "^5.0.0-alpha.37",
|
||||
"@material-ui/icons": "^5.0.0-alpha.37",
|
||||
"@material-ui/styles": "^5.0.0-alpha.35",
|
||||
"@prisma/client": "^3.0.2",
|
||||
"@prisma/client": "^3.1.1",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"colorette": "^1.2.2",
|
||||
@@ -29,7 +29,7 @@
|
||||
"formik": "^2.2.9",
|
||||
"multer": "^1.4.2",
|
||||
"next": "11.1.1",
|
||||
"prisma": "^3.0.2",
|
||||
"prisma": "^3.1.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dropzone": "^11.3.2",
|
||||
@@ -55,6 +55,6 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/workflow-testing.git"
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
}
|
||||
}
|
||||
|
||||
39
prisma/migrations/20210924045900_delete_url/migration.sql
Normal file
39
prisma/migrations/20210924045900_delete_url/migration.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `InvisibleUrl` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `Url` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Image" DROP CONSTRAINT "Image_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_imageId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "InvisibleUrl" DROP CONSTRAINT "InvisibleUrl_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Url" DROP CONSTRAINT "Url_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "InvisibleUrl";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Url";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Theme" ADD CONSTRAINT "Theme_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleImage" ADD CONSTRAINT "InvisibleImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "InvisibleImage.invis_unique" RENAME TO "InvisibleImage_invis_key";
|
||||
34
prisma/migrations/20210924050753_new_url/migration.sql
Normal file
34
prisma/migrations/20210924050753_new_url/migration.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Url" (
|
||||
"id" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"views" INTEGER NOT NULL DEFAULT 0,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Url_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "InvisibleUrl" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"invis" TEXT NOT NULL,
|
||||
"urlId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "InvisibleUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Url_id_key" ON "Url"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_invis_key" ON "InvisibleUrl"("invis");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "InvisibleUrl_urlId_unique" ON "InvisibleUrl"("urlId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Url" ADD CONSTRAINT "Url_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InvisibleUrl" ADD CONSTRAINT "InvisibleUrl_urlId_fkey" FOREIGN KEY ("urlId") REFERENCES "Url"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;
|
||||
@@ -50,26 +50,26 @@ model Image {
|
||||
}
|
||||
|
||||
model InvisibleImage {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
imageId Int
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model Url {
|
||||
id Int @id @default(autoincrement())
|
||||
to String
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
id String @id @unique
|
||||
destination String
|
||||
vanity String?
|
||||
created_at DateTime @default(now())
|
||||
views Int @default(0)
|
||||
invisible InvisibleUrl?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model InvisibleUrl {
|
||||
id Int
|
||||
url Url @relation(fields: [id], references: [id])
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
urlId String
|
||||
url Url @relation(fields: [urlId], references: [id])
|
||||
}
|
||||
|
||||
@@ -34,17 +34,15 @@ function shouldUseYarn() {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const config = readConfig();
|
||||
await validateConfig(config);
|
||||
const a = readConfig();
|
||||
const config = await validateConfig(a);
|
||||
|
||||
const data = await prismaRun(config.core.database_url, ['migrate', 'status'], true);
|
||||
if (data.includes('Following migration have not yet been applied:')) {
|
||||
if (data.includes('Following migrations have not yet been applied:')) {
|
||||
Logger.get('database').info('some migrations are not applied, applying them now...');
|
||||
await deployDb(config);
|
||||
Logger.get('database').info('finished applying migrations');
|
||||
} else {
|
||||
Logger.get('database').info('migrations up to date');
|
||||
}
|
||||
} else Logger.get('database').info('migrations up to date');
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
@@ -52,7 +50,7 @@ function shouldUseYarn() {
|
||||
const app = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: dev
|
||||
quiet: dev,
|
||||
}, config.core.port, config.core.host);
|
||||
|
||||
await app.prepare();
|
||||
@@ -70,15 +68,15 @@ function shouldUseYarn() {
|
||||
where: {
|
||||
OR: [
|
||||
{ file: parts[2] },
|
||||
{ invisible:{ invis: decodeURI(parts[2]) } }
|
||||
]
|
||||
{ invisible:{ invis: decodeURI(parts[2]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true
|
||||
}
|
||||
invisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
@@ -95,7 +93,7 @@ function shouldUseYarn() {
|
||||
|
||||
await prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } }
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
|
||||
@@ -1,45 +1,34 @@
|
||||
const Logger = require('../src/lib/logger');
|
||||
const yup = require('yup');
|
||||
|
||||
function dot(str, obj) {
|
||||
return str.split('.').reduce((a,b) => a[b], obj);
|
||||
}
|
||||
|
||||
const path = (path, type) => ({ path, type });
|
||||
const validator = yup.object({
|
||||
core: yup.object({
|
||||
secure: yup.bool().default(false),
|
||||
secret: yup.string().min(8).required(),
|
||||
host: yup.string().default('0.0.0.0'),
|
||||
port: yup.number().default(3000),
|
||||
database_url: yup.string().required(),
|
||||
}).required(),
|
||||
uploader: yup.object({
|
||||
route: yup.string().required(),
|
||||
length: yup.number().default(6),
|
||||
directory: yup.string().required(),
|
||||
admin_limit: yup.number().default(104900000),
|
||||
user_limit: yup.number().default(104900000),
|
||||
disabled_extensions: yup.array().default([]),
|
||||
}).required(),
|
||||
urls: yup.object({
|
||||
route: yup.string().required(),
|
||||
length: yup.number().default(6),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
module.exports = async config => {
|
||||
const paths = [
|
||||
path('core.secure', 'boolean'),
|
||||
path('core.secret', 'string'),
|
||||
path('core.host', 'string'),
|
||||
path('core.port', 'number'),
|
||||
path('core.database_url', 'string'),
|
||||
path('uploader.route', 'string'),
|
||||
path('uploader.length', 'number'),
|
||||
path('uploader.directory', 'string'),
|
||||
path('uploader.admin_limit', 'number'),
|
||||
path('uploader.user_limit', 'number'),
|
||||
path('uploader.disabled_extentions', 'object'),
|
||||
];
|
||||
|
||||
let errors = 0;
|
||||
|
||||
for (let i = 0, L = paths.length; i !== L; ++i) {
|
||||
const path = paths[i];
|
||||
const value = dot(path.path, config);
|
||||
if (value === undefined) {
|
||||
Logger.get('config').error(`there was no ${path.path} in config which was required`);
|
||||
++errors;
|
||||
}
|
||||
|
||||
const type = typeof value;
|
||||
if (value !== undefined && type !== path.type) {
|
||||
Logger.get('config').error(`expected ${path.type} on ${path.path}, but got ${type}`);
|
||||
++errors;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors !== 0) {
|
||||
Logger.get('config').error(`exiting due to ${errors} errors`);
|
||||
process.exit(1);
|
||||
}
|
||||
module.exports = config => {
|
||||
try {
|
||||
return validator.validateSync(config, { abortEarly: false });
|
||||
} catch (e) {
|
||||
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Config } from './types';
|
||||
import readConfig from './readConfig';
|
||||
import validateConfig from '../../server/validateConfig';
|
||||
|
||||
if (!global.config) global.config = readConfig() as Config;
|
||||
if (!global.config) global.config = validateConfig(readConfig()) as unknown as Config;
|
||||
|
||||
export default global.config;
|
||||
@@ -14,21 +14,17 @@ export default function login() {
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
|
||||
const res = await useFetch('/api/user');
|
||||
|
||||
if (res.error) return router.push('/auth/login');
|
||||
|
||||
dispatch(updateUser(res));
|
||||
|
||||
setUser(res);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loading && user) return;
|
||||
load();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { format } = require('fecha');
|
||||
const { yellow, blueBright, magenta, red, cyan } = require('colorette');
|
||||
const { blueBright, red, cyan } = require('colorette');
|
||||
|
||||
class Logger {
|
||||
static get(clas) {
|
||||
@@ -19,7 +19,7 @@ class Logger {
|
||||
}
|
||||
|
||||
error(error) {
|
||||
console.log(this.formatMessage('ERROR', this.name, error.toString()));
|
||||
console.log(this.formatMessage('ERROR', this.name, error.stack ?? error));
|
||||
}
|
||||
|
||||
formatMessage(level, name, message) {
|
||||
@@ -31,10 +31,6 @@ class Logger {
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
return cyan('INFO ');
|
||||
case 'DEBUG':
|
||||
return yellow('DEBUG');
|
||||
case 'WARN':
|
||||
return magenta('WARN ');
|
||||
case 'ERROR':
|
||||
return red('ERROR');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const { existsSync, readFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const Logger = require('./logger');
|
||||
|
||||
const e = (val, type, fn, required = true) => ({ val, type, fn, required });
|
||||
const e = (val, type, fn) => ({ val, type, fn });
|
||||
|
||||
const envValues = [
|
||||
e('SECURE', 'boolean', (c, v) => c.core.secure = v),
|
||||
@@ -10,12 +10,16 @@ const envValues = [
|
||||
e('HOST', 'string', (c, v) => c.core.host = v),
|
||||
e('PORT', 'number', (c, v) => c.core.port = v),
|
||||
e('DATABASE_URL', 'string', (c, v) => c.core.database_url = v),
|
||||
|
||||
e('UPLOADER_ROUTE', 'string', (c, v) => c.uploader.route = v),
|
||||
e('UPLOADER_LENGTH', 'number', (c, v) => c.uploader.length = v),
|
||||
e('UPLOADER_DIRECTORY', 'string', (c, v) => c.uploader.directory = v),
|
||||
e('UPLOADER_ADMIN_LIMIT', 'number', (c, v) => c.uploader.admin_limit = v),
|
||||
e('UPLOADER_USER_LIMIT', 'number', (c, v) => c.uploader.user_limit = v),
|
||||
e('UPLOADER_DISABLED_EXTS', 'array', (c, v) => v ? c.uploader.disabled_extentions = v : c.uploader.disabled_extentions = [], false),
|
||||
e('UPLOADER_DISABLED_EXTS', 'array', (c, v) => v ? c.uploader.disabled_extentions = v : c.uploader.disabled_extentions = []),
|
||||
|
||||
e('URLS_ROUTE', 'string', (c, v) => c.urls.route = v),
|
||||
e('URLS_LENGTH', 'number', (c, v) => c.urls.length = v),
|
||||
];
|
||||
|
||||
module.exports = () => {
|
||||
@@ -46,29 +50,28 @@ function tryReadEnv() {
|
||||
directory: undefined,
|
||||
admin_limit: undefined,
|
||||
user_limit: undefined,
|
||||
disabled_extentions: undefined
|
||||
}
|
||||
disabled_extentions: undefined,
|
||||
},
|
||||
urls: {
|
||||
route: undefined,
|
||||
length: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0, L = envValues.length; i !== L; ++i) {
|
||||
const envValue = envValues[i];
|
||||
let value = process.env[envValue.val];
|
||||
|
||||
if (envValue.required && !value) {
|
||||
Logger.get('config').error(`there is no config file or required environment variables (${envValue.val})... exiting...`);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
envValues[i].fn(config, value);
|
||||
if (envValue.required) {
|
||||
if (!value) {
|
||||
envValues[i].fn(config, undefined);
|
||||
} else {
|
||||
envValues[i].fn(config, value);
|
||||
if (envValue.type === 'number') value = parseToNumber(value);
|
||||
else if (envValue.type === 'boolean') value = parseToBoolean(value);
|
||||
else if (envValue.type === 'array') value = parseToArray(value);
|
||||
envValues[i].fn(config, value);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,16 @@ export interface ConfigUploader {
|
||||
disabled_extentions: string[];
|
||||
}
|
||||
|
||||
export interface ConfigUrls {
|
||||
// The route urls will be served on
|
||||
route: string;
|
||||
|
||||
// Length of random chars to generate for urls
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
core: ConfigCore;
|
||||
uploader: ConfigUploader;
|
||||
urls: ConfigUrls;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { hash, verify } from 'argon2';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import prisma from './prisma';
|
||||
import { InvisibleImage } from '@prisma/client';
|
||||
import { InvisibleImage, InvisibleUrl } from '@prisma/client';
|
||||
|
||||
export async function hashPassword(s: string): Promise<string> {
|
||||
return await hash(s);
|
||||
@@ -89,21 +89,21 @@ export function bytesToRead(bytes: number) {
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
export function createInvisURL(length: number) {
|
||||
export function randomInvis(length: number) {
|
||||
// some parts from https://github.com/tycrek/ass/blob/master/generators/lengthGen.js
|
||||
const invisibleCharset = ['\u200B', '\u2060', '\u200C', '\u200D'];
|
||||
|
||||
return [...randomBytes(length)].map((byte) => invisibleCharset[Number(byte) % invisibleCharset.length]).join('').slice(1).concat(invisibleCharset[0]);
|
||||
}
|
||||
|
||||
export function createInvis(length: number, imageId: number) {
|
||||
export function createInvisImage(length: number, imageId: number) {
|
||||
const retry = async (): Promise<InvisibleImage> => {
|
||||
const invis = createInvisURL(length);
|
||||
const invis = randomInvis(length);
|
||||
|
||||
const existing = await prisma.invisibleImage.findUnique({
|
||||
where: {
|
||||
invis
|
||||
}
|
||||
invis,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) return retry();
|
||||
@@ -111,12 +111,37 @@ export function createInvis(length: number, imageId: number) {
|
||||
const inv = await prisma.invisibleImage.create({
|
||||
data: {
|
||||
invis,
|
||||
imageId
|
||||
}
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
return inv;
|
||||
};
|
||||
|
||||
return retry();
|
||||
}
|
||||
|
||||
export function createInvisURL(length: number, urlId: string) {
|
||||
const retry = async (): Promise<InvisibleUrl> => {
|
||||
const invis = randomInvis(length);
|
||||
|
||||
const existing = await prisma.invisibleUrl.findUnique({
|
||||
where: {
|
||||
invis,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) return retry();
|
||||
|
||||
const ur = await prisma.invisibleUrl.create({
|
||||
data: {
|
||||
invis,
|
||||
urlId,
|
||||
},
|
||||
});
|
||||
|
||||
return ur;
|
||||
};
|
||||
|
||||
return retry();
|
||||
}
|
||||
@@ -58,64 +58,86 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const id = context.params.id[1];
|
||||
const route = context.params.id[0];
|
||||
if (route !== config.uploader.route.substring(1)) return {
|
||||
notFound: true
|
||||
};
|
||||
const routes = [config.uploader.route.substring(1), config.urls.route.substring(1)];
|
||||
if (!routes.includes(route)) return { notFound: true };
|
||||
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: id },
|
||||
{ invisible: { invis: id } }
|
||||
]
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
userId: true,
|
||||
embed: true
|
||||
}
|
||||
});
|
||||
if (route === routes[1]) {
|
||||
const url = await prisma.url.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ id },
|
||||
{ vanity: id },
|
||||
{ invisible: { invis: id } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
if (!url) return { notFound: true };
|
||||
|
||||
if (!image) return { notFound: true };
|
||||
return {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: url.destination,
|
||||
},
|
||||
};
|
||||
|
||||
if (!image.embed) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
} else {
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: id },
|
||||
{ invisible: { invis: id } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
userId: true,
|
||||
embed: true,
|
||||
},
|
||||
});
|
||||
if (!image) return { notFound: true };
|
||||
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
if (!image.embed) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
embedTitle: true,
|
||||
embedColor: true,
|
||||
username: true
|
||||
},
|
||||
where: {
|
||||
id: image.userId
|
||||
}
|
||||
});
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
select: {
|
||||
embedTitle: true,
|
||||
embedColor: true,
|
||||
username: true,
|
||||
},
|
||||
where: {
|
||||
id: image.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image.mimetype.startsWith('image')) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
if (!image.mimetype.startsWith('image')) {
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
return {
|
||||
props: {
|
||||
image,
|
||||
title: user.embedTitle,
|
||||
color: user.embedColor,
|
||||
username: user.username,
|
||||
normal: config.uploader.route,
|
||||
embed: image.embed
|
||||
}
|
||||
};
|
||||
return {
|
||||
props: {
|
||||
image,
|
||||
title: user.embedTitle,
|
||||
color: user.embedColor,
|
||||
username: user.username,
|
||||
normal: config.uploader.route,
|
||||
embed: image.embed,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
39
src/pages/api/shorten.ts
Normal file
39
src/pages/api/shorten.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import zconfig from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvisURL, randomChars } from 'lib/util';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method !== 'POST') return res.forbid('no allow');
|
||||
if (!req.headers.authorization) return res.forbid('no authorization');
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
token: req.headers.authorization,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return res.forbid('authorization incorect');
|
||||
if (!req.body) return res.error('no body');
|
||||
if (!req.body.url) return res.error('no url');
|
||||
const rand = randomChars(zconfig.urls.length);
|
||||
|
||||
let invis;
|
||||
const url = await prisma.url.create({
|
||||
data: {
|
||||
id: rand,
|
||||
vanity: req.body.vanity ?? null,
|
||||
destination: req.body.url,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (req.headers.zws) invis = await createInvisURL(zconfig.urls.length, url.id);
|
||||
|
||||
Logger.get('url').info(`User ${user.username} (${user.id}) shortenned a url ${url.destination} (${url.id})`);
|
||||
|
||||
return res.json({ url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.urls.route}/${req.body.vanity ? req.body.vanity : invis ? invis.invis : url.id}` });
|
||||
}
|
||||
|
||||
export default withZipline(handler);
|
||||
@@ -2,7 +2,7 @@ import multer from 'multer';
|
||||
import prisma from 'lib/prisma';
|
||||
import zconfig from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvis, randomChars } from 'lib/util';
|
||||
import { createInvisImage, randomChars } from 'lib/util';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import Logger from 'lib/logger';
|
||||
@@ -46,7 +46,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
}
|
||||
});
|
||||
|
||||
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
|
||||
if (req.headers.zws) invis = await createInvisImage(zconfig.uploader.length, image.id);
|
||||
|
||||
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), file.buffer);
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
|
||||
@@ -11,6 +11,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
let images = await prisma.image.findMany({
|
||||
take,
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
orderBy: {
|
||||
created_at: 'desc'
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { tryGetPreviewData } from 'next/dist/server/api-utils';
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
if (!user) return res.forbid('not logged in');
|
||||
if (!user.administrator) return res.forbid('you arent an administrator');
|
||||
if (!user.administrator) return res.forbid('you aren\'t an administrator');
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (req.body.id === user.id) return res.forbid('you can\'t delete your own account');
|
||||
@@ -28,7 +28,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
delete deleteUser.password;
|
||||
return res.json(deleteUser);
|
||||
} else {
|
||||
const all_users = await prisma.user.findMany({
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
username: true,
|
||||
id: true,
|
||||
@@ -40,7 +40,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
systemTheme: true
|
||||
}
|
||||
});
|
||||
return res.json(all_users);
|
||||
return res.json(users);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Files from 'components/pages/Files';
|
||||
|
||||
export default function ImagesPage() {
|
||||
export default function FilesPage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
@@ -19,4 +19,4 @@ export default function ImagesPage() {
|
||||
);
|
||||
}
|
||||
|
||||
ImagesPage.title = 'Zipline - Gallery';
|
||||
FilesPage.title = 'Zipline - Gallery';
|
||||
@@ -24,8 +24,8 @@ export default function UploadPage({ route }) {
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
return {
|
||||
props: {
|
||||
route: config.uploader.route
|
||||
}
|
||||
route: config.uploader.route,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ export default function UsersPage() {
|
||||
);
|
||||
}
|
||||
|
||||
UsersPage.title = 'Zipline - User';
|
||||
UsersPage.title = 'Zipline - Users';
|
||||
36
yarn.lock
36
yarn.lock
@@ -602,22 +602,22 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||
|
||||
"@prisma/client@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1"
|
||||
integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ==
|
||||
"@prisma/client@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.1.1.tgz#f4012631528049c22d12b212846dcf503db33cfe"
|
||||
integrity sha512-8ud8vVFMIg37yrkZ4wPpjKoMxFbCL0Pesq5eyLnag/s0LTKsVEN7ZBIQq9JzWW+AUqOzGKXr2Jt4Sl8xdGI99w==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
"@prisma/engines-version" "3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f"
|
||||
|
||||
"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
|
||||
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779"
|
||||
integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ==
|
||||
"@prisma/engines-version@3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f":
|
||||
version "3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f.tgz#f9908eb7808f2a546634398063942eaecb2474ef"
|
||||
integrity sha512-EuEMKLuwIcBO7uInZQHeG1yaywcfl32Tq8TDf5tgLvblk+ka70sej7S67lh3BV5gXMLTc3GdthSHPfDqZEK5uA==
|
||||
|
||||
"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
|
||||
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621"
|
||||
integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg==
|
||||
"@prisma/engines@3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f":
|
||||
version "3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f.tgz#7b45708e6a42523dc9bc2214e5c62781f608dc3a"
|
||||
integrity sha512-6NEp0VlLho3hVtIvj2P4h0e19AYqQSXtFGts8gSIXDnV+l5pRFZaDMfGo2RiLMR0Kfrs8c3ZYxYX0sWmVL0tWw==
|
||||
|
||||
"@reduxjs/toolkit@^1.6.0":
|
||||
version "1.6.0"
|
||||
@@ -4434,12 +4434,12 @@ prepend-http@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||
|
||||
prisma@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.2.tgz#e86cb6abf4a815c7ac97b9d0ed383f01c253ce34"
|
||||
integrity sha512-TyOCbtWGDVdWvsM1RhUzJXoGClXGalHhyYWIc5eizSF8T1ScGiOa34asBUdTnXOUBFSErbsqMNw40DHAteBm1A==
|
||||
prisma@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.1.1.tgz#4c13c35dd3a58af9134008c8ed0fdc21a632802c"
|
||||
integrity sha512-+eZtWIL6hnOKUOvqq9WLBzSw2d/EbTmOx1Td1LI8/0XE40ctXMLG2N1p6NK5/+yivGaoNJ9PDpPsPL9lO4nJrQ==
|
||||
dependencies:
|
||||
"@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
|
||||
"@prisma/engines" "3.1.0-24.c22652b7e418506fab23052d569b85d3aec4883f"
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.1"
|
||||
|
||||
Reference in New Issue
Block a user