Skip to content

Commit

Permalink
feat(Api): add pastebin api routes (#443)
Browse files Browse the repository at this point in the history
* feat(Api): add pastebin get details api route

* feat(Api): Add pastebin delete api route

* feat(Api): add pastebin create api route
  • Loading branch information
ijsKoud committed Oct 16, 2023
1 parent 4fe4b6b commit 2d2474d
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 0 deletions.
38 changes: 38 additions & 0 deletions apps/server/src/routes/api/v1/bins/[id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type Domain from "#components/Domain.js";
import type Server from "#server.js";
import { ApplyOptions, Route, methods } from "@snowcrystals/highway";
import type { NextFunction, Request, Response } from "express";
import { readFile } from "node:fs/promises";
import _ from "lodash";

@ApplyOptions<Route.Options>({ ratelimit: { max: 20, windowMs: 1e3 }, middleware: [[methods.GET, "user-api-key"]] })
export default class ApiRoute extends Route<Server> {
public async [methods.GET](req: Request, res: Response, next: NextFunction, { domain }: Record<"domain", Domain>) {
const name = req.params.id;

try {
const pastebin = await this.server.prisma.pastebin.findFirst({ where: { id: name, domain: domain.domain } });
if (!pastebin) {
res.status(404).send({
errors: [{ field: "name", code: "BIN_NOT_FOUND", message: "A pastebin with the provided name does not exist" }]
});
return;
}

const content = await readFile(pastebin.path, "utf-8");
const filtered = _.pick(pastebin, ["date", "domain", "highlight", "id", "views", "visible"]);
res.status(200).json({ ...filtered, content });
} catch (err) {
this.server.logger.fatal("[PASTEBIN:CREATE]: Fatal error while fetching a pastebin", err);
res.status(500).send({
errors: [
{
field: null,
code: "INTERNAL_SERVER_ERROR",
message: "Unknown error occurred while fetching a pastebin, please try again later."
}
]
});
}
}
}
98 changes: 98 additions & 0 deletions apps/server/src/routes/api/v1/bins/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type Domain from "#components/Domain.js";
import { Auth } from "#lib/Auth.js";
import Config from "#lib/Config.js";
import { Utils } from "#lib/utils.js";
import type Server from "#server.js";
import { ApplyOptions, Route, methods } from "@snowcrystals/highway";
import type { NextFunction, Request, Response } from "express";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { ZodError, z } from "zod";

@ApplyOptions<Route.Options>({ ratelimit: { max: 5, windowMs: 1e3 }, middleware: [[methods.POST, "user-api-key"]] })
export default class ApiRoute extends Route<Server> {
public async [methods.POST](req: Request, res: Response, next: NextFunction, { domain }: Record<"domain", Domain>) {
const body = this.parseBody(req.body);
if (body instanceof ZodError) {
const errors = Utils.parseZodError(body);
res.status(400).send({ errors });
return;
}

const config = Config.getEnv();
const pastebins = await this.server.prisma.pastebin.findMany({ where: { domain: domain.domain } });

try {
const path = join(domain.pastebinPath, `${Auth.generateToken(32)}.txt`);
const id = Utils.generateId(domain.nameStrategy, domain.nameLength) || (Utils.generateId("id", domain.nameLength) as string);

// Authentication stuff
const authBuffer = Buffer.from(`${Auth.generateToken(32)}.${Date.now()}.${domain.domain}.${id}`).toString("base64");
const authSecret = Auth.encryptToken(authBuffer, config.encryptionKey);

await writeFile(path, body.content);
const pastebin = await this.server.prisma.pastebin.create({
data: {
id: body.name ? (pastebins.map((bin) => bin.id).includes(body.name) ? id : body.name) : id,
password: body.password ? Auth.encryptPassword(body.password, config.encryptionKey) : undefined,
date: new Date(),
domain: domain.domain,
visible: body.visible,
highlight: body.highlight,
authSecret,
path
}
});

domain.auditlogs.register("Pastebin Created", `Id: ${id}`);
res.status(200).json({
url: `${req.protocol}://${domain}/bins/${id}`,
visible: pastebin.visible,
password: Boolean(pastebin.password),
highlight: pastebin.highlight,
date: pastebin.date,
domain: pastebin.domain
});
} catch (err) {
this.server.logger.fatal("[BIN:CREATE]: Fatal error while creating a pastebin", err);
res.status(500).send({
errors: [
{
field: null,
code: "INTERNAL_SERVER_ERROR",
message: "Unknown error occurred while creating a pastebin, please try again later."
}
]
});
}
}

/**
* Parses the request body
* @param body The body to parse
* @returns Parsed json content or ZodError if there was a parsing issue
*/
private parseBody(body: any) {
const schema = z.object({
name: z
.string({ invalid_type_error: "Property 'name' must be a string" })
.refine((arg) => !arg.includes("/"), "Name cannot contain a slash (/)")
.optional(),
visible: z.boolean({ required_error: "Visibility state is required", invalid_type_error: "Property 'visible' must be a boolean" }),
content: z
.string({ required_error: "Pastebin content is required", invalid_type_error: "Property 'content' must be a string" })
.min(1, "Pastebin content is required"),
highlight: z.string({
required_error: "A valid highlight type is required",
invalid_type_error: "Property 'highlight' must be a string"
}),
password: z.string({ invalid_type_error: "Property 'password' must be a string" }).optional()
});

try {
return schema.parse(body);
} catch (error) {
return error as ZodError;
}
}
}
59 changes: 59 additions & 0 deletions apps/server/src/routes/api/v1/bins/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type Domain from "#components/Domain.js";
import { Utils } from "#lib/utils.js";
import type Server from "#server.js";
import { ApplyOptions, Route, methods } from "@snowcrystals/highway";
import type { NextFunction, Request, Response } from "express";
import { ZodError, z } from "zod";

@ApplyOptions<Route.Options>({ ratelimit: { max: 5, windowMs: 1e3 }, middleware: [[methods.DELETE, "user-api-key"]] })
export default class ApiRoute extends Route<Server> {
public async [methods.DELETE](req: Request, res: Response, next: NextFunction, { domain }: Record<"domain", Domain>) {
const body = this.parseBody(req.body);
if (body instanceof ZodError) {
const errors = Utils.parseZodError(body);
res.status(400).send({ errors });
return;
}

try {
const pastebin = await this.server.prisma.pastebin.findFirst({ where: { id: body.name, domain: domain.domain } });
if (!pastebin) {
res.status(404).send({
errors: [{ field: "name", code: "BIN_NOT_FOUND", message: "A pastebin with the provided name does not exist" }]
});
return;
}

await this.server.prisma.pastebin.delete({ where: { id_domain: { domain: domain.domain, id: body.name } } });
res.sendStatus(204);
} catch (err) {
this.server.logger.fatal("[PASTEBIN:CREATE]: Fatal error while deleting a pastebin", err);
res.status(500).send({
errors: [
{
field: null,
code: "INTERNAL_SERVER_ERROR",
message: "Unknown error occurred while deleting a pastebin, please try again later."
}
]
});
}
}

/**
* Parses the request body
* @param body The body to parse
* @returns Parsed json content or ZodError if there was a parsing issue
*/
private parseBody(body: any) {
const schema = z.object({
name: z.string({ invalid_type_error: "Property 'name' must be a string", required_error: "The pastebin name is required" })
});

try {
return schema.parse(body);
} catch (error) {
return error as ZodError;
}
}
}

0 comments on commit 2d2474d

Please sign in to comment.