diff --git a/client/lib/render-markdown.tsx b/client/lib/render-markdown.tsx index de8aebb5..04bfcb86 100644 --- a/client/lib/render-markdown.tsx +++ b/client/lib/render-markdown.tsx @@ -1,4 +1,4 @@ -import { marked, Lexer } from 'marked' +import { marked } from 'marked' import Highlight, { defaultProps, Language, } from 'prism-react-renderer' import { renderToStaticMarkup } from 'react-dom/server' diff --git a/client/pages/api/markdown/[id].ts b/client/pages/api/markdown/[id].ts index 3cf942a9..4d450131 100644 --- a/client/pages/api/markdown/[id].ts +++ b/client/pages/api/markdown/[id].ts @@ -11,8 +11,8 @@ const renderMarkdown: NextApiHandler = async (req, res) => { Authorization: `Bearer ${req.cookies["drift-token"]}` } }) - console.log(file.status) - if (file.status !== 200) { + if (file.status + !== 200) { return res.status(404).json({ error: "File not found" }) } diff --git a/client/pages/api/raw/[id].ts b/client/pages/api/raw/[id].ts index 7325c2b6..401e7803 100644 --- a/client/pages/api/raw/[id].ts +++ b/client/pages/api/raw/[id].ts @@ -14,7 +14,6 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Cache-Control", "s-maxage=86400") if (file.ok) { const json = await file.json() - console.log(json) const data = json const { title, content } = data // serve the file raw as plain text diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx index 3327ce24..e99da842 100644 --- a/client/pages/post/[id].tsx +++ b/client/pages/post/[id].tsx @@ -21,8 +21,8 @@ export const getStaticPaths: GetStaticPaths = async () => { }) const json = await posts.json() - const filtered = json.filter((post: any) => post.visibility === "public" || post.visibility === "unlisted") - const paths = filtered.map((post: any) => ({ + const filtered = json.filter((post: Post) => post.visibility === "public" || post.visibility === "unlisted") + const paths = filtered.map((post: Post) => ({ params: { id: post.id } })) diff --git a/server/package.json b/server/package.json index 2af531dd..e80e987c 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ "dependencies": { "bcrypt": "^5.0.1", "body-parser": "^1.18.2", + "celebrate": "^15.0.1", "cors": "^2.8.5", "dotenv": "^16.0.0", "express": "^4.16.2", @@ -40,6 +41,7 @@ "@types/node": "^17.0.21", "@types/react-dom": "^17.0.14", "ts-node": "^10.6.0", + "tsconfig-paths": "^3.14.1", "tslint": "^6.1.3", "typescript": "^4.6.2" } diff --git a/server/src/app.ts b/server/src/app.ts index 71a86704..c7eb8a58 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -2,7 +2,8 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as errorhandler from 'strong-error-handler'; import * as cors from 'cors'; -import { posts, users, auth, files } from './routes'; +import { posts, users, auth, files } from '@routes/index'; +import { errors } from 'celebrate' export const app = express(); @@ -19,7 +20,10 @@ app.use("/posts", posts) app.use("/users", users) app.use("/files", files) +app.use(errors()); + app.use(errorhandler({ debug: process.env.ENV !== 'production', log: true, })); + diff --git a/server/src/lib/render-markdown.tsx b/server/src/lib/render-markdown.tsx index de8aebb5..04bfcb86 100644 --- a/server/src/lib/render-markdown.tsx +++ b/server/src/lib/render-markdown.tsx @@ -1,4 +1,4 @@ -import { marked, Lexer } from 'marked' +import { marked } from 'marked' import Highlight, { defaultProps, Language, } from 'prism-react-renderer' import { renderToStaticMarkup } from 'react-dom/server' diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 9e77a93a..bade025d 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,9 +1,10 @@ import { Router } from 'express' import { genSalt, hash, compare } from "bcrypt" -import { User } from '../lib/models/User' +import { User } from '@lib/models/User' import { sign } from 'jsonwebtoken' -import config from '../lib/config' -import jwt from '../lib/middleware/jwt' +import config from '@lib/config' +import jwt from '@lib/middleware/jwt' +import { celebrate, Joi } from 'celebrate' const NO_EMPTY_SPACE_REGEX = /^\S*$/ @@ -13,90 +14,108 @@ console.log(`Registration password required: ${requiresServerPassword}`) export const auth = Router() const validateAuthPayload = (username: string, password: string, serverPassword?: string): void => { - if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) { - throw new Error("Authentication data does not fulfill requirements") - } + if (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) { + throw new Error("Authentication data does not fulfill requirements") + } - if (requiresServerPassword) { - if (!serverPassword || process.env.REGISTRATION_PASSWORD !== serverPassword) { - throw new Error("Server password is incorrect. Please contact the server administrator.") - } + if (requiresServerPassword) { + if (!serverPassword || process.env.REGISTRATION_PASSWORD !== serverPassword) { + throw new Error("Server password is incorrect. Please contact the server administrator.") } + } } -auth.post('/signup', async (req, res, next) => { - try { - validateAuthPayload(req.body.username, req.body.password, req.body.serverPassword) - - const username = req.body.username.toLowerCase(); - - const existingUser = await User.findOne({ where: { username: username } }) - if (existingUser) { - throw new Error("Username already exists") - } - - const salt = await genSalt(10) - const user = { - username: username as string, - password: await hash(req.body.password, salt) - } - - const created_user = await User.create(user); - - const token = generateAccessToken(created_user.id); - - res.status(201).json({ token: token, userId: created_user.id }) - } catch (e) { - next(e); +auth.post('/signup', + celebrate({ + body: { + username: Joi.string().required(), + password: Joi.string().required(), + serverPassword: Joi.string().required().allow('', null), } -}); + }), + async (req, res, next) => { + try { + validateAuthPayload(req.body.username, req.body.password, req.body.serverPassword) + const username = req.body.username.toLowerCase(); -auth.post('/signin', async (req, res, next) => { - const error = "User does not exist or password is incorrect" + const existingUser = await User.findOne({ + where: { username: username }, + }); + if (existingUser) { + throw new Error("Username already exists"); + } + + const salt = await genSalt(10); + const user = { + username: username as string, + password: await hash(req.body.password, salt), + }; + + const created_user = await User.create(user); + + const token = generateAccessToken(created_user.id); + + res.status(201).json({ token: token, userId: created_user.id }); + } catch (e) { + next(e); + } + } +); + +auth.post( + "/signin", + celebrate({ + body: { + username: Joi.string().required(), + password: Joi.string().required(), + serverPassword: Joi.string().required().allow('', null), + }, + }), + async (req, res, next) => { + const error = "User does not exist or password is incorrect"; const errorToThrow = new Error(error); try { - if (!req.body.username || !req.body.password) { - throw errorToThrow - } + if (!req.body.username || !req.body.password) { + throw errorToThrow; + } - const username = req.body.username.toLowerCase(); - const user = await User.findOne({ where: { username: username } }); - if (!user) { - throw errorToThrow - } - - const password_valid = await compare(req.body.password, user.password); - if (password_valid) { - const token = generateAccessToken(user.id); - res.status(200).json({ token: token, userId: user.id }); - } else { - throw errorToThrow - } + const username = req.body.username.toLowerCase(); + const user = await User.findOne({ where: { username: username } }); + if (!user) { + throw errorToThrow; + } + const password_valid = await compare(req.body.password, user.password); + if (password_valid) { + const token = generateAccessToken(user.id); + res.status(200).json({ token: token, userId: user.id }); + } else { + throw errorToThrow; + } } catch (e) { - next(e); + next(e); } + } +); + +auth.get("/requires-passcode", async (req, res, next) => { + if (requiresServerPassword) { + res.status(200).json({ requiresPasscode: true }); + } else { + res.status(200).json({ requiresPasscode: false }); + } }); -auth.get('/requires-passcode', async (req, res, next) => { - if (requiresServerPassword) { - res.status(200).json({ requiresPasscode: true }); - } else { - res.status(200).json({ requiresPasscode: false }); - } -}) - function generateAccessToken(id: string) { - return sign({ id: id }, config.jwt_secret, { expiresIn: '2d' }); + return sign({ id: id }, config.jwt_secret, { expiresIn: "2d" }); } auth.get("/verify-token", jwt, async (req, res, next) => { - try { - res.status(200).json({ - message: "You are authenticated" - }) - } - catch (e) { - next(e); - } -}) + try { + res.status(200).json({ + message: "You are authenticated", + }); + } catch (e) { + next(e); + } +}); diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index 21ee2b71..3cc98f72 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -1,57 +1,72 @@ -import { Router } from 'express' -import secretKey from '../lib/middleware/secret-key'; -import { File } from '../lib/models/File' +import { celebrate, Joi } from "celebrate"; +import { Router } from "express"; +import { File } from "@lib/models/File"; +import secretKey from "@lib/middleware/secret-key"; -export const files = Router() +export const files = Router(); -files.get("/raw/:id", secretKey, async (req, res, next) => { +files.get("/raw/:id", + celebrate({ + params: { + id: Joi.string().required(), + }, + }), + secretKey, + async (req, res, next) => { try { - const file = await File.findOne({ - where: { - id: req.params.id - }, - attributes: ["title", "content"], - }) + const file = await File.findOne({ + where: { + id: req.params.id + }, + attributes: ["title", "content"], + }) + if (!file) { + return res.status(404).json({ error: "File not found" }) + } - if (!file) { - return res.status(404).json({ error: "File not found" }) - } - - // TODO: JWT-checkraw files - if (file?.post?.visibility === "private") { - // jwt(req as UserJwtRequest, res, () => { - // res.json(file); - // }) - res.json(file); - } else { - res.json(file); - } + // TODO: JWT-checkraw files + if (file?.post?.visibility === "private") { + // jwt(req as UserJwtRequest, res, () => { + // res.json(file); + // }) + res.json(file); + } else { + res.json(file); + } } catch (e) { - next(e); + next(e); } -}); + } +) -files.get("/html/:id", async (req, res, next) => { +files.get("/html/:id", + celebrate({ + params: { + id: Joi.string().required(), + }, + }), + async (req, res, next) => { try { - const file = await File.findOne({ - where: { - id: req.params.id - }, - attributes: ["html"], - }) + const file = await File.findOne({ + where: { + id: req.params.id + }, + attributes: ["html"], + }) - if (!file) { - return res.status(404).json({ error: "File not found" }) - } + if (!file) { + return res.status(404).json({ error: "File not found" }) + } - res.setHeader('Content-Type', 'text/plain') - res.setHeader('Cache-Control', 'public, max-age=4800') - res.status(200).write(file.html) - res.end() + res.setHeader('Content-Type', 'text/plain') + res.setHeader('Cache-Control', 'public, max-age=4800') + res.status(200).write(file.html) + res.end() } catch (error) { - next(error) + next(error) } -}) \ No newline at end of file + } +) diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index d4922148..bd7fe99c 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -1,4 +1,4 @@ -export { auth } from './auth'; -export { posts } from './posts'; -export { users } from './users'; -export { files } from './files'; +export { auth } from "./auth"; +export { posts } from "./posts"; +export { users } from "./users"; +export { files } from "./files"; diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index 343ed435..9a8b4575 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -1,182 +1,209 @@ -import { Router } from 'express' -// import { Movie } from '../models/Post' -import { File } from '../lib/models/File' -import { Post } from '../lib/models/Post'; -import jwt, { UserJwtRequest } from '../lib/middleware/jwt'; +import { Router } from "express"; +import { celebrate, Joi } from "celebrate"; +import { File } from '@lib/models/File' +import { Post } from '@lib/models/Post'; +import jwt, { UserJwtRequest } from '@lib/middleware/jwt'; import * as crypto from "crypto"; -import { User } from '../lib/models/User'; -import secretKey from '../lib/middleware/secret-key'; -import markdown from '../lib/render-markdown'; +import { User } from '@lib/models/User'; +import secretKey from '@lib/middleware/secret-key'; +import markdown from '@lib/render-markdown'; -export const posts = Router() +export const posts = Router(); -posts.post('/create', jwt, async (req, res, next) => { +const postVisibilitySchema = (value: string) => { + if (value === 'public' || value === 'private') { + return value; + } else { + throw new Error('Invalid post visibility'); + } +} + +posts.post( + "/create", + jwt, + celebrate({ + body: { + title: Joi.string().required(), + files: Joi.any().required(), + visibility: Joi.string().custom(postVisibilitySchema, 'valid visibility').required(), + userId: Joi.string().required(), + password: Joi.string().optional(), + }, + }), + async (req, res, next) => { try { - if (!req.body.files) { - throw new Error("Please provide files.") - } + let hashedPassword: string = '' + if (req.body.visibility === 'protected') { + hashedPassword = crypto.createHash('sha256').update(req.body.password).digest('hex'); + } - if (!req.body.title) { - throw new Error("Please provide a title.") - } + const newPost = new Post({ + title: req.body.title, + visibility: req.body.visibility, + password: hashedPassword, + }) - if (!req.body.userId) { - throw new Error("No user id provided.") - } - - if (!req.body.visibility) { - throw new Error("Please provide a visibility.") - } - - if (req.body.visibility === 'protected' && !req.body.password) { - throw new Error("Please provide a password.") - } - - let hashedPassword: string = '' - if (req.body.visibility === 'protected') { - hashedPassword = crypto.createHash('sha256').update(req.body.password).digest('hex'); - } - - const newPost = new Post({ - title: req.body.title, - visibility: req.body.visibility, - password: hashedPassword, + await newPost.save() + await newPost.$add('users', req.body.userId); + const newFiles = await Promise.all(req.body.files.map(async (file) => { + const html = getHtmlFromFile(file); + const newFile = new File({ + title: file.title, + content: file.content, + sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(), + html }) - await newPost.save() - await newPost.$add('users', req.body.userId); - const newFiles = await Promise.all(req.body.files.map(async (file) => { - const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', ''] - const fileType = () => { - const pathParts = file.title.split(".") - const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "" - return language - } - const type = fileType() - let contentToRender: string = (file.content || ''); + await newFile.$set("user", req.body.userId); + await newFile.$set("post", newPost.id); + await newFile.save(); + return newFile; + })) - if (!renderAsMarkdown.includes(type)) { - contentToRender = - `~~~${type} -${file.content} -~~~` - } else { - contentToRender = '\n' + file.content; - } - const html = markdown(contentToRender) - const newFile = new File({ - title: file.title, - content: file.content, - sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(), - html - }) + await Promise.all(newFiles.map((file) => { + newPost.$add("files", file.id); + newPost.save(); + })) - await newFile.$set("user", req.body.userId); - await newFile.$set("post", newPost.id); - await newFile.save(); - return newFile; - })) - - await Promise.all(newFiles.map((file) => { - newPost.$add("files", file.id); - newPost.save(); - })) - - res.json(newPost); + res.json(newPost); } catch (e) { - next(e); + next(e); } -}); + } +); posts.get("/", secretKey, async (req, res, next) => { - try { - const posts = await Post.findAll({ - attributes: ["id", "title", "visibility", "createdAt"], - }) - res.json(posts); - } catch (e) { - next(e); - } + try { + const posts = await Post.findAll({ + attributes: ["id", "title", "visibility", "createdAt"], + }) + + res.json(posts); + } catch (e) { + next(e); + } }); posts.get("/mine", jwt, secretKey, async (req: UserJwtRequest, res, next) => { - if (!req.user) { - return res.status(401).json({ error: "Unauthorized" }) - } + if (!req.user) { + return res.status(401).json({ error: "Unauthorized" }) + } - try { - const user = await User.findByPk(req.user.id, { - include: [ - { - model: Post, - as: "posts", - include: [ - { - model: File, - as: "files" - } - ] - }, - ], - }) - if (!user) { - return res.status(404).json({ error: "User not found" }) - } - return res.json(user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())) - } catch (error) { - next(error) + try { + const user = await User.findByPk(req.user.id, { + include: [ + { + model: Post, + as: "posts", + include: [ + { + model: File, + as: "files" + } + ] + }, + ], + }) + if (!user) { + return res.status(404).json({ error: "User not found" }) } + return res.json(user.posts?.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())) + } catch (error) { + next(error) + } }) -posts.get("/:id", async (req, res, next) => { +posts.get( + "/:id", + celebrate({ + params: { + id: Joi.string().required(), + }, + }), + async (req: UserJwtRequest, res, next) => { try { - const post = await Post.findOne({ - where: { - id: req.params.id - }, - include: [ - { - model: File, - as: "files", - attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"], - }, - { - model: User, - as: "users", - attributes: ["id", "username"], - }, - ] + const post = await Post.findOne({ + where: { + id: req.params.id, + }, + include: [ + { + model: File, + as: "files", + attributes: [ + "id", + "title", + "content", + "sha", + "createdAt", + "updatedAt", + ], + }, + { + model: User, + as: "users", + attributes: ["id", "username"], + }, + ], + }); + + if (!post) { + return res.status(404).json({ error: "Post not found" }) + } + + // if public or unlisted, cache + if (post.visibility === 'public' || post.visibility === 'unlisted') { + res.set('Cache-Control', 'public, max-age=4800') + } + + if (post.visibility === 'public' || post?.visibility === 'unlisted') { + secretKey(req, res, () => { + res.json(post); }) - - if (!post) { - throw new Error("Post not found.") - } - - - if (post.visibility === 'public' || post?.visibility === 'unlisted') { - secretKey(req, res, () => { - res.json(post); - }) - } else if (post.visibility === 'private') { - jwt(req as UserJwtRequest, res, () => { - res.json(post); - }) - } else if (post.visibility === 'protected') { - const { password } = req.query - if (!password || typeof password !== 'string') { - return jwt(req as UserJwtRequest, res, () => { - res.json(post); - }) - } - const hash = crypto.createHash('sha256').update(password).digest('hex').toString() - if (hash !== post.password) { - return res.status(400).json({ error: "Incorrect password." }) - } - + } else if (post.visibility === 'private') { + jwt(req as UserJwtRequest, res, () => { + res.json(post); + }) + } else if (post.visibility === 'protected') { + const { password } = req.query + if (!password || typeof password !== 'string') { + return jwt(req as UserJwtRequest, res, () => { res.json(post); + }) } + const hash = crypto.createHash('sha256').update(password).digest('hex').toString() + if (hash !== post.password) { + return res.status(400).json({ error: "Incorrect password." }) + } + + res.json(post); + } } catch (e) { - next(e); + next(e); } -}); + } +); + +function getHtmlFromFile(file: any) { + const renderAsMarkdown = ['markdown', 'md', 'mdown', 'mkdn', 'mkd', 'mdwn', 'mdtxt', 'mdtext', 'text', '']; + const fileType = () => { + const pathParts = file.title.split("."); + const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : ""; + return language; + }; + const type = fileType(); + let contentToRender: string = (file.content || ''); + + if (!renderAsMarkdown.includes(type)) { + contentToRender = + `~~~${type} +${file.content} +~~~`; + } else { + contentToRender = '\n' + file.content; + } + const html = markdown(contentToRender); + return html; +} + diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts index aedcd1e1..94be3c01 100644 --- a/server/src/routes/users.ts +++ b/server/src/routes/users.ts @@ -1,16 +1,14 @@ -import { Router } from 'express' -// import { Movie } from '../models/Post' -import { User } from '../lib/models/User' -import jwt from '../lib/middleware/jwt' +import { Router } from "express"; +// import jwt from "@lib/middleware/jwt"; +// import { User } from "@lib/models/User"; -export const users = Router() - -users.get('/', jwt, async (req, res, next) => { - try { - const allUsers = await User.findAll() - res.json(allUsers) - } catch (error) { - next(error) - } -}) +export const users = Router(); +// users.get("/", jwt, async (req, res, next) => { +// try { +// const allUsers = await User.findAll(); +// res.json(allUsers); +// } catch (error) { +// next(error); +// } +// }); diff --git a/server/tsconfig.json b/server/tsconfig.json index eb71dbf0..6cf7b311 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -14,7 +14,16 @@ "strictNullChecks": true, "skipLibCheck": true, "strictPropertyInitialization": true, - "outDir": "dist" + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@routes/*": ["./src/routes/*"], + "@lib/*": ["./src/lib/*"] + } + }, + "ts-node": { + // Do not forget to `npm i -D tsconfig-paths` + "require": ["tsconfig-paths/register"] }, "include": ["index.ts", "src/**/*.ts"], "exclude": ["node_modules"] diff --git a/server/yarn.lock b/server/yarn.lock index 300c8a41..7eaf6f5a 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -35,6 +35,18 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@hapi/hoek@^9.0.0": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" + integrity sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.8" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz#32abc8a5c624bc4e46c43d84dfb8b26d33a96f58" @@ -50,6 +62,23 @@ semver "^7.3.5" tar "^6.1.11" +"@sideway/address@^4.1.3": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27" + integrity sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" + integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -150,6 +179,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/jsonwebtoken@^8.5.8": version "8.5.8" resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz#01b39711eb844777b7af1d1f2b4cf22fda1c0c44" @@ -506,6 +540,15 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +celebrate@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/celebrate/-/celebrate-15.0.1.tgz#767fa6268f7446b473ea69cd6326ce4dffee4d1e" + integrity sha512-K2y221k10u+K2t9w25802qXh8h1mVWZf+6pl7zHdlhhwzrOSQFnnw+GsR8k17oyn4Y3fVErBGsO/+CeW8N7aRQ== + dependencies: + escape-html "1.0.3" + joi "17.x.x" + lodash "4.17.x" + chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -858,7 +901,7 @@ escape-goat@^2.0.0: resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@~1.0.3: +escape-html@1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -1413,6 +1456,17 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" +joi@17.x.x: + version "17.6.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" + integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1458,6 +1512,13 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + jsonwebtoken@^8.1.0, jsonwebtoken@^8.5.1: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" @@ -1557,7 +1618,7 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash@^4.17.20, lodash@^4.17.21: +lodash@4.17.x, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1687,6 +1748,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + minipass@^3.0.0: version "3.1.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" @@ -2385,6 +2451,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -2515,6 +2586,16 @@ ts-node@^10.6.0: v8-compile-cache-lib "^3.0.0" yn "3.1.1" +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.13.0, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"