feat(v3.2.4): url shortenning

This commit is contained in:
diced 2021-09-24 20:31:45 -07:00
parent a9d0be8aae
commit 3451bd8762
No known key found for this signature in database
GPG key ID: 85AB64C74535D76E
19 changed files with 300 additions and 110 deletions

View file

@ -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',
},
};

View file

@ -13,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'" > 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

View file

@ -1,6 +1,6 @@
{
"name": "zip3",
"version": "3.2.3",
"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",

View 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";

View 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;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Url" ADD COLUMN "vanity" TEXT;

View file

@ -57,17 +57,19 @@ model InvisibleImage {
}
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])
}

View file

@ -34,10 +34,11 @@ function shouldUseYarn() {
(async () => {
try {
const config = await validateConfig(readConfig());
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');
@ -49,7 +50,7 @@ function shouldUseYarn() {
const app = next({
dir: '.',
dev,
quiet: dev
quiet: dev,
}, config.core.port, config.core.host);
await app.prepare();
@ -67,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) {
@ -92,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);

View file

@ -18,11 +18,16 @@ const validator = yup.object({
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 => {
module.exports = config => {
try {
return await validator.validate(config, { abortEarly: false });
return validator.validateSync(config, { abortEarly: false });
} catch (e) {
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
}

View file

@ -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;

View file

@ -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) {

View file

@ -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 = []),
e('URLS_ROUTE', 'string', (c, v) => c.urls.route = v),
e('URLS_LENGTH', 'number', (c, v) => c.urls.length = v),
];
module.exports = () => {
@ -46,8 +50,12 @@ 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) {

View file

@ -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;
}

View file

@ -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();
}

View file

@ -58,62 +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
View 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);

View file

@ -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})`);

View file

@ -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,
},
};
};

View file

@ -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"