diff --git a/backend/app.js b/backend/app.js index b17ab58..5088d54 100644 --- a/backend/app.js +++ b/backend/app.js @@ -5,6 +5,7 @@ const authRoutes = require("./routes/auth"); const userRoutes = require("./routes/user"); const roomRoutes = require("./routes/room"); const mongoose = require("mongoose"); +const cookieParser = require("cookie-parser"); require("dotenv").config(); mongoose @@ -17,14 +18,15 @@ const http = require("http"); const server = http.createServer(app); const { socketIOServerInit } = require("./socket"); -app.use(cors({ origin: "*" })); +app.use(cors({ origin: "http://localhost:5173", credentials: true })); app.use(bodyParser.json()); app.use((req, res, next) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); + // res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173"); + // res.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE"); + // res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); next(); }); +app.use(cookieParser()); socketIOServerInit(server); @@ -40,9 +42,11 @@ app.use("/api/room", roomRoutes); app.use((error, req, res, next) => { const status = error.status || 500; - const message = error.message || "Something went wrong."; - console.log(error) - res.status(status).json({ message: message }); + console.log(error); + res.status(status).json({ + userMessage: "Something went wrong", + devMessage: error?.message || "Internal server error", + }); }); server.listen(8080, () => { diff --git a/backend/package-lock.json b/backend/package-lock.json index e88a6f1..635effe 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "chess.js": "^1.0.0-beta.6", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -19,7 +20,8 @@ "mongoose": "^7.2.1", "nodemailer": "^6.9.3", "socket.io": "^4.6.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" } }, "node_modules/@socket.io/component-emitter": { @@ -177,6 +179,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -1225,6 +1247,14 @@ "optional": true } } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/backend/package.json b/backend/package.json index 9c4282f..2fe3fa7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "chess.js": "^1.0.0-beta.6", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -21,6 +22,7 @@ "mongoose": "^7.2.1", "nodemailer": "^6.9.3", "socket.io": "^4.6.1", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "zod": "^3.21.4" } } diff --git a/backend/routes/auth.js b/backend/routes/auth.js index d734c4a..d386989 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,93 +1,122 @@ const express = require("express"); -const { createJSONToken, isValidPassword, validateJSONToken, generatePasswordHash } = require("../util/auth"); +const { + createJSONToken, + isValidPassword, + validateJSONToken, + generatePasswordHash, + checkAuth, +} = require("../util/auth"); const { isValidEmail, isValidText } = require("../util/validation"); const { User } = require("../models/user"); +const { z, ZodError } = require("zod"); const router = express.Router(); +const loginSchema = z + .object({ + username: z + .string() + .min(5, { message: "Username should not be less than 5 characters" }) + .max(15, { message: "Username should not be more than 15 characters" }), + password: z + .string() + .min(8, { message: "Password should not be less than 8 characters" }) + .max(15, { message: "Password should not be more than 15 characters" }), + }) + .required(); + +const signupSchema = z + .object({ + username: z + .string() + .min(5, { message: "Username should not be less than 5 characters" }) + .max(15, { message: "Username should not be more than 15 characters" }), + email: z.string().email({ message: "Please enter a valid email address" }), + password: z + .string() + .min(8, { message: "Password should not be less than 8 characters" }) + .max(15, { message: "Password should not be more than 15 characters" }), + }) + .required(); + router.post("/signup", async (req, res, next) => { - console.log(req.body); - const data = { email: req.body.email, password: req.body.password, username: req.body.username }; - let errors = {}; - - if (!isValidEmail(data.email)) { - errors.email = "Invalid email."; - } else { - try { - let user = await User.findOne({ email: data.email }); - if (user) { - errors.email = "Email exists already."; - } - } catch (error) { - throw error; - } - } - - if (!isValidText(data.password, 6)) { - errors.password = "Password must be at least 6 characters long."; - } - - let user = await User.findOne({ username: data.username }); - if (user) errors.username = "Username already taken"; - - if (Object.keys(errors).length > 0) { - console.log(errors); - return res.status(409).json({ - success: false, - message: "User signup failed due to validation errors.", - errors, - }); - } - try { + const data = { email: req.body.email, password: req.body.password, username: req.body.username }; + + signupSchema.parse(data); + + // check if username or email already exists + if (await User.findOne({ username: data.username })) + return res.status(409).json({ error: { username: "username already taken" } }); + if (await User.findOne({ email: data.email })) + return res.status(409).json({ error: { email: "user with this email already exists" } }); + let userData = { email: data.email, username: data.username, password_hash: await generatePasswordHash(data.password), }; - let userDoc = new User(userData); - await userDoc.save(); - console.log(userDoc.id) + let userDoc = await User.create(userData); const authToken = createJSONToken(userDoc.id); const { id, username, email } = userDoc; - res.status(201).json({ + res.status(201).cookie("auth-token", authToken, { httpOnly: true, sameSite: "strict" }).json({ success: true, - message: "User created.", user: { id, username, email }, token: authToken, }); + } catch (err) { + if (err instanceof ZodError) { + return res.status(400).json({ userMessage: "Invalid data submitted", devMessage: "Invalid schema" }); + } + next(err); + } +}); + +router.post("/login", async (req, res, next) => { + try { + let username = req.body.username, + password = req.body.password; + + loginSchema.parse({ username, password }); + + let user; + user = await User.findOne({ username }); + if (!user) + return res.status(404).json({ + userMessage: "User does not exist", + devMessage: "'username' not found in db", + }); + + const pwIsValid = await isValidPassword(password, user.password_hash); + if (!pwIsValid) { + return res.status(401).json({ + success: false, + userMessage: "Invalid credentials", + devMessage: "Invalid credentials", + }); + } + + const token = createJSONToken(user.id); + res.cookie("auth-token", token, { httpOnly: true, sameSite: "strict" }); + return res + .status(200) + .json({ token, user: { id: user.id, username: user.username, email: user.email }, success: true }); } catch (error) { + if (error instanceof ZodError) { + return res.status(401).json({ userMessage: "Invalid Credentials", devMessage: "Invalid schema" }); + } next(error); } }); -router.post("/login", async (req, res) => { - const username = req.body.username; - const password = req.body.password; - - let user; +router.delete("/logout", checkAuth, (req, res, next) => { try { - user = await User.findOne({ username }); - if (!user) return res.status(401).json({ message: "User not found" }); - } catch (error) { - return res.status(401).json({ success: false, message: "AutheFntication failed." }); + res.clearCookie("auth-token", { httpOnly: true, sameSite: "strict" }); + res.status(200).json({ success: true }); + } catch (err) { + next(err); } - - const pwIsValid = await isValidPassword(password, user.password_hash); - if (!pwIsValid) { - return res.status(422).json({ - success: false, - message: "Invalid credentials.", - errors: { credentials: "Invalid email or password entered." }, - }); - } - - const token = createJSONToken(user.id); - console.log(username) - return res - .status(200) - .json({ token, user: { id: user.id, username: user.username, email: user.email }, success: true }); }); module.exports = router; diff --git a/backend/util/auth.js b/backend/util/auth.js index 0331e0b..a7d4db4 100644 --- a/backend/util/auth.js +++ b/backend/util/auth.js @@ -21,27 +21,45 @@ function isValidPassword(password, storedPassword) { return compare(password, storedPassword); } +// function checkAuthMiddleware(req, res, next) { +// if (req.method === "OPTIONS") { +// return next(); +// } +// if (!req.headers.authorization) { +// console.log("NOT AUTH. AUTH HEADER MISSING."); +// return next(new NotAuthError("Not authenticated.")); +// } +// const authFragments = req.headers.authorization.split(" "); + +// if (authFragments.length !== 2) { +// console.log("NOT AUTH. AUTH HEADER INVALID."); +// return next(new NotAuthError("Not authenticated.")); +// } +// const authToken = authFragments[1]; +// try { +// const validatedToken = validateJSONToken(authToken); +// req.userid = validatedToken; +// } catch (error) { +// console.log("NOT AUTH. TOKEN INVALID."); +// return next(new NotAuthError("Not authenticated.")); +// } +// next(); +// } + function checkAuthMiddleware(req, res, next) { if (req.method === "OPTIONS") { return next(); } - if (!req.headers.authorization) { - console.log("NOT AUTH. AUTH HEADER MISSING."); - return next(new NotAuthError("Not authenticated.")); + let authToken = req.cookies["auth-token"]; + if (!authToken) { + return res.status(401).json({ userMessage: "Not authenticated", devMessage: "Auth token not found" }); } - const authFragments = req.headers.authorization.split(" "); - - if (authFragments.length !== 2) { - console.log("NOT AUTH. AUTH HEADER INVALID."); - return next(new NotAuthError("Not authenticated.")); - } - const authToken = authFragments[1]; try { const validatedToken = validateJSONToken(authToken); req.userid = validatedToken; } catch (error) { console.log("NOT AUTH. TOKEN INVALID."); - return next(new NotAuthError("Not authenticated.")); + return res.status(401).json({ userMessage: "Not authenticated", devMessage: "Invalid auth token" }); } next(); }