Skip to content

Commit

Permalink
feat: redesign (#404)
Browse files Browse the repository at this point in the history
* refactor(Logo): change colours and font

* refactor(Ui): rename ui to components

* feat: add shadcn/ui

* feat(Web): add landing page

* fix: minor bug fixes with importing

* feat(Utils): add notFoundError class to handle 404 error message

* feat: add not-found page redesign

* refactor(Ui): move components outside src directory

* feat(Components): add theme toggle

* feat(Ui): add select menu

* feat(Utils): add useful nextjs page types

* feat: add theme toggle to pages

* feat(Ui): add form components

* refactor: add redesigned login page

* feat(Login): add light mode

* fix(Login): redirect error

* refactor: move auth layout to @paperplane/components

* fix(Login): user not redirected to correct domain

* refactor: add redesigned reset page

* feat(Ui): add dialog component

* refactor(SignUp): add redesigned page

* feat(Components): remove un-used modal

* feat(SignUp): remove useless return statement

* feat(Ui): add avatar component

* feat(Ui): add sheet component

* feat(Components): add redesigned navbars

* feat(Ui): add table component

* fix(Navbar): minor bug fixes

* fix(Navbar): more bug fixes

* refactor(Dashboard): add redesigned home page

* feat(Ui): add skeleton component

* feat: add redesigned changelog modal

* feat(Ui): add toast component

* feat(Ui): add switch component

* feat(DashboardHome): make more responsive for mobile

* feat(Ui): add checkbox component

* feat(Api): add new url create route

* refactor(ShorturlDashboard): add redesigned page

* feat(AuditlogTable): update border color for darkmode

* refactor: re-position files and components

* feat(Ui): add tabs component

* feat(Api): add password field to paste-bin create api

* feat(Ui): add textarea component

* refactor(DashboardPastebin): add redesigned page

* feat(Components): remove un-used files

* feat(Server): add new upload api endpoint

* refactor(FilesDashboard): add redesigned page

* fix(Server): minor bug fixes

* feat(Ui): add tooltip component

* fix(Api): minor bug fixes

* feat(FilesDashboard): add grid view

* fix(FilesDashboard): minor bug fixes

* feat(DashboardSettings): add Api key section

* feat(DashboardSettings): add settings form

* feat(DashboardSettings): add embed settings

* feat(DashboardSettingsApi): add embed settings to response

* feat(Ui): add alert-dialog

* feat(DashboardSettings): add big red buttons section

* chore: remove old settings page

* chore(Components): remove un-used files

* refactor(Components): move glitch component to new location

* fix(Navbar): onClick listener attached incorrectly

* feat(Components): add admin layout

* feat(AdminPanel): add lib

* feat(AdminPanel): add stats section

* feat(AdminPanel): add auditlog

* refactor(AdminPanel): add new page design

* feat(Dashboard): add alert dialog for bulk delete

* feat(AdminUserPanel): add user table

* feat(Server): small change to api

* feat(Ui): add scroll-area component

* feat(AdminUserPanel): add edit dialog

* fix(UpdateDialog): minor bug fixes

* fix(UpdateDialog): minor bug fixes

* fix(UpdateDialog): minor bug fixes

* fix: critical bug fixes

* feat(AdminUserPanel): add create dialog

* refactor(AdminUserPanel): add redesigned page

* feat(Api): add auth mode api

* feat(AdminPanelSettings): add authMode component

* feat(Server): update settings api

* feat(AdminPanelSettings): add settings form

* fix: bug fixes

* feat(AdminPanelSettings): add big red buttons component

* chore: remove un-used imports

* feat(AdminPanelSettings): add sign up domain dialog

* feat(AdminPanelSettings): add invite dialog

* feat(AdminPanelSettings): add backups dialog

* refactor(AdminPanelSettings): add redesigned page

* fix: critical bug fix

* refactor(FilesAuth): add redesigned page

* refactor(BinAuth): add redesigned page

* chore: remove un-used files

* refactor(PasteBin): add redesigned page

* feat(Files): add redesigned page

* chore: remove un-used files

* chore: regen lockfile

* fix: add missing progress circle
  • Loading branch information
ijsKoud committed Sep 11, 2023
1 parent aad4ded commit deacaa6
Show file tree
Hide file tree
Showing 249 changed files with 11,617 additions and 7,556 deletions.
4 changes: 3 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"build": "tsc --build",
"start": "node ./dist/index.js",
"lint": "eslint src",
"dev:watch": "tsc --watch",
"build:watch": "tsc --watch",
"dev": "NODE_ENV=development nodemon ./dist"
},
"peerDependencies": {
Expand All @@ -26,6 +26,7 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^6.10.0",
"formidable": "^3.5.1",
"fuse.js": "^6.6.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
Expand All @@ -48,6 +49,7 @@
"@types/cron": "^2.0.1",
"@types/express": "^4.17.17",
"@types/express-rate-limit": "^6.0.0",
"@types/formidable": "^3.4.2",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash": "^4.14.198",
"@types/mime-types": "^2.1.1",
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/api/admin/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default async function handler(server: Server, req: Request, res: Respons
}

data.extensions = data.extensions.filter((ext) => ext.startsWith(".") && !ext.endsWith("."));
const domain = data.domain.startsWith("*.") ? `` : data.domain;
const domain = data.domain.startsWith("*.") ? `${data.extension}.${data.domain.replace("*.", "")}` : data.domain;

await server.domains.create({
disabled: false,
Expand All @@ -68,7 +68,7 @@ export default async function handler(server: Server, req: Request, res: Respons
await server.prisma.signupDomain.delete({ where: { domain: data.domain } });
}

server.adminAuditLogs.register("Create User", `User: ${data.domain} (${server.domains.get(data.domain)!.filesPath})`);
server.adminAuditLogs.register("Create User", `User: ${data.domain} (${server.domains.get(domain)!.filesPath})`);
res.sendStatus(204);
return;
} catch (err) {
Expand Down
6 changes: 0 additions & 6 deletions apps/server/src/api/admin/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ export default async function handler(server: Server, req: Request, res: Respons
return;
}

if (!["2fa", "password"].includes(data.authMode)) {
res.status(400).send({ message: "Invalid authMode provided" });
return;
}

if (!["open", "closed", "invite"].includes(data.signUpMode)) {
res.status(400).send({ message: "Invalid signUpMode provided" });
return;
Expand All @@ -51,7 +46,6 @@ export default async function handler(server: Server, req: Request, res: Respons
data.extensions = data.extensions.filter((ext) => ext.startsWith(".") && !ext.endsWith("."));
await server.config.update(data);

if (server.envConfig.authMode !== data.authMode) await server.domains.resetAuth();
server.adminAuditLogs.register("Default Settings Update", "N/A");
res.sendStatus(204);
} catch (err) {
Expand Down
31 changes: 31 additions & 0 deletions apps/server/src/api/admin/settings/auth-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Response, Request } from "express";
import { Auth } from "../../../lib/Auth.js";
import type { Middleware, RequestMethods } from "../../../lib/types.js";
import type Server from "../../../Server.js";

export default async function handler(server: Server, req: Request, res: Response) {
if (req.method === "GET") {
res.send(server.envConfig.authMode);
return;
}

if (req.method === "POST") {
try {
const data = req.body as { mode: "2fa" | "password" };
if (!["2fa", "password"].includes(data.mode)) {
res.status(400).send({ message: "Invalid mode provided" });
return;
}

if (server.envConfig.authMode !== data.mode) await server.domains.resetAuth();
server.adminAuditLogs.register("Default Settings Update", "N/A");
res.sendStatus(204);
} catch (err) {
server.logger.fatal(`[AUTH-MODE:POST]: Fatal error while creating a new PaperPlane account `, err);
res.status(500).send({ message: "Internal server error occured, please try again later." });
}
}
}

export const methods: RequestMethods[] = ["get", "post"];
export const middleware: Middleware[] = [Auth.adminMiddleware.bind(Auth)];
3 changes: 1 addition & 2 deletions apps/server/src/api/bins/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ export default async function handler(server: Server, req: Request, res: Respons
const hostName = proxyHost ? proxyHost : req.headers.host ?? req.hostname;
const host = server.domains.domains.find((dm) => dm.domain.startsWith(Array.isArray(hostName) ? hostName[0] : hostName));

const { id: _id } = req.params;
const [id] = _id.split(".");
const { id } = req.params;
const pastebin = await server.prisma.pastebin.findFirst({ where: { domain: host?.domain ?? "", id } });

if (!pastebin) {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/api/dashboard/files/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function handler(server: Server, req: DashboardRequest, res

const files = await server.prisma.file.findMany({ where: { domain: req.locals.domain.domain } });
const data = req.body as FilesEditFormBody;
if (typeof data.name !== "string" || data.name.includes(".") || data.name.includes("/")) {
if (typeof data.name !== "string" || data.name.includes("/")) {
res.status(400).send({ message: "Invalid file name provided" });
return;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/api/dashboard/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default async function handler(server: Server, req: DashboardRequest, res
views: file.views,
visible: file.visible,
ext: Utils.getExtension(file.mimeType || lookup(file.path) || "") || "",
url: `${Utils.getProtocol()}${req.locals.domain}/files/${file.id}.${file.path.split(".").filter(Boolean).slice(1).join(".")}`
url: `${Utils.getProtocol()}${req.locals.domain}/files/${file.id}`
}));

res.send({
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/api/dashboard/paste-bins/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export default async function handler(server: Server, req: DashboardRequest, res
domain: req.locals.domain.domain,
path,
highlight: data.highlight,
authSecret
authSecret,
password: data.password ? Auth.encryptPassword(data.password, server.envConfig.encryptionKey) : undefined
}
});

Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/api/dashboard/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export default async function handler(server: Server, req: DashboardRequest, res
tokens: req.locals.domain.apiTokens.map((token) => ({ name: token.name, date: token.date })),
nameLength: req.locals.domain.nameLength,
nameStrategy: req.locals.domain.nameStrategy,
embedEnabled: req.locals.domain.embedEnabled
embedEnabled: req.locals.domain.embedEnabled,
embed: {
title: req.locals.domain.embedTitle,
description: req.locals.domain.embedDescription,
color: req.locals.domain.embedColor
}
});

return;
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/api/dashboard/urls/[id].ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Response } from "express";
import type { NextFunction, Response } from "express";
import { Auth } from "../../../lib/Auth.js";
import type { DashboardRequest, UrlEditFormBody, Middleware, RequestMethods } from "../../../lib/types.js";
import type Server from "../../../Server.js";

export default async function handler(server: Server, req: DashboardRequest, res: Response) {
export default async function handler(server: Server, req: DashboardRequest, res: Response, next: NextFunction) {
const { id } = req.params;
if (id === "create") return next();

if (!req.locals.domain) {
res.status(500).send({ message: "Internal server error, please try again later." });
return;
Expand Down
38 changes: 38 additions & 0 deletions apps/server/src/api/dashboard/urls/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Response } from "express";
import { Auth } from "../../../lib/Auth.js";
import { type DashboardRequest, type Middleware, type RequestMethods } from "../../../lib/types.js";
import { Utils } from "../../../lib/utils.js";
import type Server from "../../../Server.js";

export default async function handler(server: Server, req: DashboardRequest, res: Response) {
if (!req.locals.domain) {
res.status(500).send({ message: "Internal server error, please try again later." });
return;
}

const { name, visible, url } = req.body;
if (typeof url !== "string" || typeof visible !== "boolean") {
res.status(400).send({ message: "No shorturl data provided" });
return;
}

const links = await server.prisma.url.findMany({ where: { domain: req.locals.domain.domain } });
const strategy = req.locals.domain.nameStrategy === "name" ? "id" : req.locals.domain.nameStrategy;
let path = typeof name === "string" ? name : "";

if (!path.length || links.find((link) => link.id === path)) {
path = Utils.generateId(strategy, req.locals.domain.nameLength) as string;
while (links.find((link) => link.id === path)) path = Utils.generateId(strategy, req.locals.domain.nameLength) as string;
}

try {
await server.prisma.url.create({ data: { date: new Date(), url, id: path, visible, domain: req.locals.domain.domain } });
res.send(`${Utils.getProtocol()}${req.locals.domain.domain}/r/${path}`);
} catch (err) {
server.logger.fatal(`[UPLOAD:POST]: Fatal error while uploading a shorturl`, err);
res.status(500).send({ message: "Internal server error occured, please try again later." });
}
}

export const methods: RequestMethods[] = ["post"];
export const middleware: Middleware[] = [Auth.userMiddleware.bind(Auth)];
3 changes: 1 addition & 2 deletions apps/server/src/api/files/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ export default async function handler(server: Server, req: Request, res: Respons
const hostName = proxyHost ? proxyHost : req.headers.host ?? req.hostname;
const host = server.domains.domains.find((dm) => dm.domain.startsWith(Array.isArray(hostName) ? hostName[0] : hostName));

const { id: _id } = req.params;
const [id] = _id.split(".");
const { id } = req.params;
const file = await server.prisma.file.findFirst({ where: { domain: host?.domain ?? "", id } });

const embed = host?.embedEnabled
Expand Down
85 changes: 85 additions & 0 deletions apps/server/src/api/v1/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Response } from "express";
import { Auth, Domain, type DashboardRequest, type Middleware, type RequestMethods, Utils } from "../../lib/index.js";
import formidable from "formidable";
import type Server from "../../Server.js";

export default async function handler(server: Server, req: DashboardRequest, res: Response) {
if (!req.locals.domain) {
res.status(500).send({ message: "Internal server error, please try again later." });
return;
}

const form = formidable({
filter: (part) => fileFilter(part, req.locals.domain),
filename: (name, ext) => `${Auth.generateToken(32)}${ext}`,
uploadDir: req.locals.domain.filesPath,
keepExtensions: true,
maxFileSize: getSize(req.locals.domain.uploadSize),
maxFieldsSize: getSize(req.locals.domain.maxStorage === 0 ? 0 : req.locals.domain.maxStorage - req.locals.domain.storage)
});

try {
const [fields, files] = await form.parse(req);
const uploadedFiles = files.file;
if (!uploadedFiles) {
res.status(400).send({ errors: [{ field: "upload", code: "MISSING_UPLOAD_FILES", message: "Missing uploaded files." }] });
return;
}

const name = fields.name?.[0];
const password = fields.password?.[0];
const visible = fields.visible?.[0];
if (uploadedFiles.length === 1) {
const file = await req.locals.domain.registerUpload(uploadedFiles[0], { name, password, visible: visible === "true" ? true : false });
const fileUrl = `${Utils.getProtocol()}${req.locals.domain}/files/${file}`;
res.send({ url: fileUrl, files: { [uploadedFiles[0].originalFilename!]: fileUrl } });
return;
}

const registedFiles = await Promise.all(
uploadedFiles.map(async (file) => ({ url: await req.locals.domain.registerUpload(file), name: file.originalFilename! }))
);
res.send({
url: registedFiles[0],
files: registedFiles
.map((file) => ({ [file.name]: `${Utils.getProtocol()}${req.locals.domain}/files/${file.url}` }))
.reduce((a, b) => ({ ...a, ...b }), {})
});
} catch (err) {
server.logger.fatal(`[UPLOAD:POST]: Fatal error while uploading a file`, err);
res.status(500).send({
errors: [{ field: undefined, code: "INTERNAL_ERROR", message: "Internal server error occured, please try again later." }]
});
}
}

/**
* Filters out disallowed files
* @param part The formidable content part
* @param domain The domain the upload is coming from
* @returns
*/
const fileFilter = (part: formidable.Part, domain: Domain) => {
const { extensions, extensionsMode, disabled } = domain;
if (disabled || !part.originalFilename || !part.mimetype) return false;

const ext = part.originalFilename.split(".").filter(Boolean).slice(1).join(".");
switch (extensionsMode) {
case "block":
if (extensions.includes(ext)) return false;
break;
case "pass":
if (!extensions.includes(ext)) return false;
break;
}

return true;
};

const getSize = (size: number): number | undefined => {
if (size < 0) return 0;
return size === 0 ? undefined : size;
};

export const methods: RequestMethods[] = ["post"];
export const middleware: Middleware[] = [Auth.userApiKeyMiddleware.bind(Auth)];
7 changes: 3 additions & 4 deletions apps/server/src/lib/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class Api {
await Promise.all(files.map((filePath) => this.loadFile(filePath)));

this.server.express.get("/files/:file", rateLimit({ max: 2e2, windowMs: 1e3 }), async (req, res) => {
const { file: _fileName } = req.params;
const { file: fileName } = req.params;

const domain = this.server.domains.get(req.headers.host || req.hostname);
if (!domain) {
Expand All @@ -31,7 +31,6 @@ export class Api {
return;
}

const fileName = _fileName.includes(".") ? _fileName.split(".")[0] : _fileName;
const file = await this.server.prisma.file.findFirst({ where: { domain: domain.domain, id: fileName } });

const checkForAuth = () => {
Expand Down Expand Up @@ -60,13 +59,13 @@ export class Api {
};

if (Boolean(file.password) && !checkForAuth() && !checkForPassword()) {
res.redirect(`/files/${_fileName}/auth`);
res.redirect(`/files/${fileName}/auth`);
return;
}

if (!req.query.raw) {
if (domain.embedEnabled || charset(lookup(file.path.split(/\//g).reverse()[0]) || "") === "UTF-8") {
await this.server.next.render(req, res, `/files/${_fileName}`);
await this.server.next.render(req, res, `/files/${fileName}`);
return;
}
}
Expand Down
Loading

0 comments on commit deacaa6

Please sign in to comment.