This repository has been archived by the owner on Dec 12, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Api): add pastebin api routes (#443)
* 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
Showing
3 changed files
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
} | ||
] | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |