diff --git a/server/package.json b/server/package.json index 1a78e7cd..117a016d 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,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", diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 7a3aa7e7..9c646fd5 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -1,85 +1,104 @@ -import { Router } from 'express' -import { genSalt, hash, compare } from "bcrypt" -import { User } from '../../lib/models/User' -import { sign } from 'jsonwebtoken' -import config from '../../lib/config' -import jwt from '../../lib/middleware/jwt' +import { Router } from "express"; +import { genSalt, hash, compare } from "bcrypt"; +import { User } from "../../lib/models/User"; +import { sign } from "jsonwebtoken"; +import config from "../../lib/config"; +import jwt from "../../lib/middleware/jwt"; +import { celebrate, Joi } from "celebrate"; -const NO_EMPTY_SPACE_REGEX = /^\S*$/ +const NO_EMPTY_SPACE_REGEX = /^\S*$/; -export const auth = Router() +export const auth = Router(); const validateAuthPayload = (username: string, password: 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"); + } +}; -auth.post('/signup', async (req, res, next) => { +auth.post( + "/signup", + celebrate({ + params: { + username: Joi.string().required(), + password: Joi.string().required(), + }, + }), + async (req, res, next) => { try { - validateAuthPayload(req.body.username, req.body.password) + validateAuthPayload(req.body.username, req.body.password); - const username = req.body.username.toLowerCase(); + const username = req.body.username.toLowerCase(); - const existingUser = await User.findOne({ where: { username: username } }) - if (existingUser) { - throw new Error("Username already exists") - } + 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 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 created_user = await User.create(user); - const token = generateAccessToken(created_user.id); + const token = generateAccessToken(created_user.id); - res.status(201).json({ token: token, userId: created_user.id }) + res.status(201).json({ token: token, userId: created_user.id }); } catch (e) { - next(e); + next(e); } -}); + } +); -auth.post('/signin', async (req, res, next) => { - const error = "User does not exist or password is incorrect" +auth.post( + "/signin", + celebrate({ + params: { + username: Joi.string().required(), + password: Joi.string().required(), + }, + }), + 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); } -}); + } +); 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 7a23751a..067fad39 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -1,29 +1,36 @@ -import { Router } from 'express' +import { celebrate, Joi } from "celebrate"; +import { Router } from "express"; // import { Movie } from '../models/Post' -import { File } from '../../lib/models/File' +import { File } from "../../lib/models/File"; -export const files = Router() +export const files = Router(); -files.get("/raw/:id", async (req, res, next) => { +files.get( + "/raw/:id", + celebrate({ + params: { + id: Joi.string().required(), + }, + }), + async (req, res, next) => { try { - const file = await File.findOne({ - where: { - id: req.params.id - }, - attributes: ["title", "content"], - }) - // TODO: fix post inclusion - // if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') { - res.setHeader("Cache-Control", "public, max-age=86400"); - res.json(file); - // } else { - // TODO: should this be `private, `? - // res.setHeader("Cache-Control", "max-age=86400"); - // res.json(file); - // } + const file = await File.findOne({ + where: { + id: req.params.id, + }, + attributes: ["title", "content"], + }); + // TODO: fix post inclusion + // if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') { + res.setHeader("Cache-Control", "public, max-age=86400"); + res.json(file); + // } else { + // TODO: should this be `private, `? + // res.setHeader("Cache-Control", "max-age=86400"); + // res.json(file); + // } + } catch (e) { + next(e); } - catch (e) { - next(e); - } -}); - + } +); 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 a528f310..04706499 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -1,97 +1,116 @@ -import { Router } from 'express' +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 { 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 { User } from "../../lib/models/User"; +import { celebrate, Joi } from "celebrate"; -export const posts = Router() +export const posts = Router(); -posts.post('/create', jwt, async (req, res, next) => { +posts.post( + "/create", + jwt, + celebrate({ + body: { + title: Joi.string().required(), + files: Joi.any().required(), + visibility: Joi.string().required(), + userId: Joi.string().required(), + }, + }), + async (req, res, next) => { + console.log(req.body); try { - if (!req.body.files) { - throw new Error("Please provide files.") - } + // Create the "post" object + const newPost = new Post({ + title: req.body.title, + visibility: req.body.visibility, + }); - if (!req.body.title) { - throw new Error("Please provide a title.") - } + await newPost.save(); + await newPost.$add("users", req.body.userId); + const newFiles = await Promise.all( + req.body.files.map(async (file) => { + // Establish a "file" for each file in the request + const newFile = new File({ + title: file.title, + content: file.content, + sha: crypto + .createHash("sha256") + .update(file.content) + .digest("hex") + .toString(), + }); - if (!req.body.userId) { - throw new Error("No user id provided.") - } - - if (!req.body.visibility) { - throw new Error("Please provide a visibility.") - } - - // Create the "post" object - const newPost = new Post({ - title: req.body.title, - visibility: req.body.visibility, + await newFile.$set("user", req.body.userId); + await newFile.$set("post", newPost.id); + await newFile.save(); + return newFile; }) + ); - await newPost.save() - await newPost.$add('users', req.body.userId); - const newFiles = await Promise.all(req.body.files.map(async (file) => { - // Establish a "file" for each file in the request - const newFile = new File({ - title: file.title, - content: file.content, - sha: crypto.createHash('sha256').update(file.content).digest('hex').toString(), - }) + 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("/:id", async (req: UserJwtRequest, 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?.visibility === 'public' || post?.visibility === 'unlisted') { - res.setHeader("Cache-Control", "public, max-age=86400"); - res.json(post); - } else { - // TODO: should this be `private, `? - res.setHeader("Cache-Control", "max-age=86400"); - jwt(req, res, () => { - res.json(post); - }); - } + if (post?.visibility === "public" || post?.visibility === "unlisted") { + res.setHeader("Cache-Control", "public, max-age=86400"); + res.json(post); + } else { + // TODO: should this be `private, `? + res.setHeader("Cache-Control", "max-age=86400"); + jwt(req, res, () => { + res.json(post); + }); + } + } catch (e) { + next(e); } - catch (e) { - next(e); - } -}); - + } +); diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts index 3ffefd01..0c397629 100644 --- a/server/src/routes/users.ts +++ b/server/src/routes/users.ts @@ -1,46 +1,47 @@ -import { Router } from 'express' -// import { Movie } from '../models/Post' -import { User } from '../../lib/models/User' -import { File } from '../../lib/models/File' -import jwt, { UserJwtRequest } from '../../lib/middleware/jwt' -import { Post } from '../../lib/models/Post' +import { Router } from "express"; +import { User } from "../../lib/models/User"; +import { File } from "../../lib/models/File"; +import jwt, { UserJwtRequest } from "../../lib/middleware/jwt"; +import { Post } from "../../lib/models/Post"; -export const users = Router() +export const users = Router(); -users.get('/', jwt, async (req, res, next) => { - try { - const allUsers = await User.findAll() - res.json(allUsers) - } catch (error) { - next(error) - } -}) +users.get("/", jwt, async (req, res, next) => { + try { + const allUsers = await User.findAll(); + res.json(allUsers); + } catch (error) { + next(error); + } +}); users.get("/mine", jwt, 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); + } +}); diff --git a/server/yarn.lock b/server/yarn.lock index 8adac559..51c2bc45 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" @@ -475,6 +504,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" @@ -822,7 +860,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= @@ -1377,6 +1415,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@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1521,7 +1570,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==