Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prikshit Rana - Case Study : StackOverflow || SwiggyIPP : Stage 2 #43

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
tags
37 changes: 37 additions & 0 deletions stackOverflow-nodeJs-App-SwiggyIPP-batch_2-Prikshit_Rana/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Stackoverflow node.js App || SwiggyIPP


The application serves as a platform for users to ask and answer questions.
<br/>Feature Implemented:<br/>
* Register
* Login
* Display/Update User Info.
* Ask/Update/Delete Question
* Ask/Update Answer

## Requirements

* Node 16.13.1
* MongoDb 5.0.5
* ExpressJS
* Git

## Common setup

* Clone the repo
* Install dependencies using following command
```bash
npm install run
```

## Steps to start server

To start the express server, run the following

```bash
npm start
```

Open [http://localhost:4000/login](http://localhost:4000/login) to login into app and start contributing.


Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import dotenv from 'dotenv'
import express from "express";
import config from "./config/index.js";
import morgan from "morgan";
import { register, login, protect } from "./utils/auth.js";
import connectDb from "./utils/db.js";
import apiUserRouter from "./routes/user.router.js"
import apiQuestionRouter from "./routes/question.router.js"
import apiAnswerRouter from "./routes/answer.router.js"

//reading environment variables
dotenv.config();

const app = express();

app.disable("x-powered-by");
app.use(express.json({ extended: false }));
app.use(morgan("dev"));

//route calls
app.post("/register", register);
app.post("/login", login);

app.use("/api", protect);
app.use('/api/user', apiUserRouter)
app.use('/api/question', apiQuestionRouter)
app.use('/api/answer', apiAnswerRouter)

/**
* start() : connect to database and start server
*/
export const start = async () => {
try {
connectDb();
app.listen(config.port, () => {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', `REST API running on http://localhost:${config.port}`);
});
} catch (e) {
console.error('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e);
}
};

// staring server
start();

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const dev = {
secrets: {
jwt: 'swiggyIPPBatch-2'
},
dbUrl: 'mongodb://127.0.0.1:27017/demoDbStackOverflow'
}

export default dev;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import lodash from 'lodash'
const env = process.env.NODE_ENV || 'development'
import dev from './dev.js'

const baseConfig = {
env,
isDev: env === 'development',
port: 4000,
secrets: {
jwt: process.env.JWT_SECRET,
jwtExp: '7d'
}
}

let envConfig = dev

console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', lodash.merge(baseConfig, envConfig))

export default lodash.merge(baseConfig, envConfig);
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Answer from "../models/answer.model.js";
import Question from "../models/question.model.js";

/**
* createAnswer : answer question based on question Id
*/
export const createAnswer = async (req, res) => {
try {
const queId = req.body.question_id;
const createdBy = req.user._id;

const answerAlreadyExist = await Answer.find({
ques_id:queId,
createdBy: createdBy
})
.select("-updatedAt, -__v")
.lean()
.exec();

if(answerAlreadyExist.length !== 0)
return res.status(401).send({ message : `Answer already posted for quesId: ${req.body.question_id} by user: ${req.user.firstName} ${req.user.lastName}, you can update your answer if required` });

const answer = await Answer.create({
ques_id: queId,
answer_body: req.body.answer_body,
createdBy: createdBy
});

const question = await Question.findByIdAndUpdate(
queId,
{ $push: { answers: answer._id } },
{ new: true, useFindAndModify: false }
)
.populate("answers")
.select("-updatedAt, -__v");

if (!question) {
res.status(400).end();
}
const status = {
message: `Answered posted to question having id: ${queId}`
};
res.status(200).send({ status, data: question });
} catch (e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e);
res.status(400).send({ message: e });
}
};

/**
* updateOne : update answer based on userId and questionId
*/
export const updateAnswer = async (req, res) => {
try{
const isQuesAnsByUser = await Answer.find({
ques_id:req.params.id,
createdBy: req.user._id
})
.select("-updatedAt, -__v")
.lean()
.exec();

if(isQuesAnsByUser.length === 0)
return res.status(401).send({ message : `No answer posted for quesId: ${req.params.id} by user: ${req.user.firstName} ${req.user.lastName}, post answer only then you can update` });

const updatedAnswer = await Answer.findOneAndUpdate({
ques_id: req.params.id,
createdBy: req.user._id
},
req.body, {new: true}
)
.select("-updatedAt, -__v")
.lean()
.exec();

if(!updatedAnswer){
return res.status(400).send({ message: "Update Answer Request failed"});
}
res.status(200).json({ message: `Answer updated for quesId: ${req.params.id}`, data: updatedAnswer})
}catch(e){
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
}

export const displayAllAnswersToQuestionById = async (req, res) => {
try {
const data = await Question.findById(req.params.id).populate(
"answers"
)
.select("-updatedAt, -__v");

return res.status(200).send({ data });
} catch (e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).send({ message: e });
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { crudOperationList } from "../utils/crud.js";
import Question from "../models/question.model.js";

export default crudOperationList(Question)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import User from '../models/user.model.js'

console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', User)

/**
* displayUserInfo : display user info
*/
export const displayUserInfo = (req, res) => {
res.status(200).json({ message: "User Info", data: req.user})
}

/**
* updateUserInfo : update user info
*/
export const updateUserIndo = async (req, res) => {
try{
const user = await User.findByIdAndUpdate(req.user._id, req.body, {
new: true
})
.select("-updatedAt, -__v")
.lean()
.exec();

res.status(200).json({message: "User Updated Successfull", data: user})
}catch(e){
return res.status(400).send({data: e});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import mongoose from "mongoose";
import validator from "validator";

const answerSchema = new mongoose.Schema(
{
ques_id: {
type: mongoose.SchemaTypes.ObjectId,
ref: "question",
required: true
},
answer_body: {
type: String,
required: true,
validate(value) {
if (validator.isEmpty(value)) {
throw new Error("Answer cannot be empty");
}
}
},
createdBy: {
type: mongoose.SchemaTypes.ObjectId,
ref: "user",
required: true
}
},

{ timestamps: true }
);

const Answer = mongoose.model("answer", answerSchema);
export default Answer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import mongoose from "mongoose";
import validator from "validator";

//question schema
const questionSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
validate(value) {
if (validator.isEmpty(value)) {
throw new Error("Title cannot be empty");
}
}
},
question_body: {
type: String,
required: true,
validate(value) {
if (validator.isEmpty(value)) {
throw new Error("Question cannot be empty");
}
}
},
createdBy: {
type: mongoose.SchemaTypes.ObjectId,
ref: "user",
required: true
},
answers: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "answer"
}
]
},

{ timestamps: true }
);

const Question = mongoose.model("question", questionSchema);
export default Question;
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import validator from "validator";

/**
* validatePassword : check if password is strong based on certain conditions
*/
const validatePassword = password => {
return validator.isStrongPassword(password, {
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
returnScore: false,
pointsPerUnique: 1,
pointsPerRepeat: 0.5,
pointsForContainingLower: 10,
pointsForContainingUpper: 10,
pointsForContainingNumber: 10,
pointsForContainingSymbol: 10
});
};

//user schema
const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: [true, "Email is required"],
unique: true,
trim: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error("Please fill valid email.");
}
},
index: true
},

password: {
type: String,
required: [true, "Password cannnot be empty"],
validate(value) {
if (!validatePassword(value)) {
throw new Error("Password validation failed. Create strong password");
}
}
},

firstName: {
type: String,
required: [true, "First name cannot be empty"]
},

lastName: {
type: String
},

developerProfile: {
company: {
type: String
},

githubId: {
type: String
},

mobileNumber: {
type: String,
required: [true, "Mobile number is required"]
}
}
},
{ timestamps: true }
);

userSchema.pre("save", function(next) {
if (!this.isModified("password")) {
return next();
}

bcrypt.hash(this.password, 8, (err, hash) => {
if (err) {
return next(err);
}
this.password = hash;
next();
});
});

/**
* checkPassword : check if password is correct
*/
userSchema.methods.checkPassword = function(password) {
const passwordHash = this.password;
return new Promise((resolve, reject) => {
bcrypt.compare(password, passwordHash, (err, same) => {
if (err) {
return reject(err);
}
resolve(same);
});
});
};

const User = mongoose.model("user", userSchema);
export default User;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createAnswer, updateAnswer, displayAllAnswersToQuestionById } from "../controller/answer.controller.js";
import express from "express";

const router = express.Router();

// api/answer : createAnswer to question by id
router.route("/")
.post(createAnswer)

// api/answer/:id : display All answer to question by Id
router.route("/:id")
.get(displayAllAnswersToQuestionById)
.put(updateAnswer)

export default router;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import questionController from "../controller/question.controller.js";
import express from "express";

const router = express.Router();

// api/question : create and findQuesion
router.route("/")
.post(questionController.createOuestion)
.get(questionController.findQuestion);

// api/question/:id : find/update/delete question by id
router
.route("/:id")
.get(questionController.findQuestionById)
.put(questionController.updateQuestion)
.delete(questionController.deleteQuestion);

export default router;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import express from 'express'
import {displayUserInfo, updateUserIndo} from '../controller/user.controller.js'
const router = express.Router()

//get user info
router.get('/', displayUserInfo)

//update user info
router.put('/', updateUserIndo)

export default router
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import config from "../config/index.js";
import User from "../models/user.model.js";
import jwt from "jsonwebtoken";

/**
* newToken : create New JWT token for user
*/
export const newToken = user => {
return jwt.sign({ id: user.id }, config.secrets.jwt, {
expiresIn: config.secrets.jwtExp
});
};

/**
* verifyToken : verify JWT token for user
*/
export const verifyToken = token =>
new Promise((resolve, reject) => {
jwt.verify(token, config.secrets.jwt, (err, payload) => {
if (err) return reject(err);
resolve(payload);
});
});

/**
* register : register user
* method-type = POST
* return-status:201
*/

export const register = async (req, res) => {
if (!req.body.email || !req.body.password) {
return res.status(400).send({ message: "Email and Password are required" });
}

try {
const user = await User.create(req.body);
const token = newToken(user);
return res.status(201).send({ message: "User registered Successfully" , registration_name: user._doc.firstName + " " + user._doc.lastName , email: user._doc.email ,access_token: token });
} catch (e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
return res.status(500).send({ message: "User Registration Failed" });
}
};

/**
* login : login user
* method-type = POST
* return-status:201 on success and 401 for invalid cred
*/
export const login = async (req, res) => {
if (!req.body.email || !req.body.password) {
return res.status(400).send({ message: "Email and Password are required" });
}

try {
const user = await User.findOne({ email: req.body.email })
.select("email password")
.exec();

if (!user) {
return res.status(401).send({ message: "Sorry invalid credentials" });
}

const match = await user.checkPassword(req.body.password);

if (!match) {
return res.status(401).send({ message: "Sorry invalid credentials" });
}

const token = newToken(user);
return res.status(201).send({ message: "User logged in successfully" , access_token: token });
} catch (e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(500).send({ message: "User login failed" });;
}
};

/**
* protect : check user is logged in before doing any operation
*/
export const protect = async (req, res, next) => {
const bearer = req.headers.authorization;
if (!bearer || !bearer.startsWith("Bearer ")) {
return res.status(401).send({ message: "Authentication Token Incorrect" });
}

const token = bearer.split("Bearer ")[1].trim();
let payload;
try {
payload = await verifyToken(token);
} catch (e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
return res.status(401).end({ message:"Token Verification failed" });
}

const user = await User.findById(payload.id)
.select("-password, -updatedAt, -__v")
.lean()
.exec();


if (!user) {
return res.status(401).send({ message: "Sorry invalid credentials" });
}

req.user = user;
next();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// functions used by questions controller for creat/update/delete question

/**
* createOne : create new Question for user based on userId
*/
export const createOne = model => async (req, res) => {
const createdBy = req.user._id;
try {
const document = await model.create({ ...req.body, createdBy });

res.status(201).json({ message: "Question posted Successfully", data: document });
} catch(e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
};

/**
* updateOne : update Question based on userId and questionId
*/
export const updateOne = model => async(req, res) => {
try{
const updatedDocument = await model.findOneAndUpdate({
createdBy: req.user._id,
_id: req.params.id
},
req.body, {new: true}
)
.select("-updatedAt, -__v")
.lean()
.exec();

if(!updatedDocument){
return res.status(400).end()
}

res.status(200).json({ message: "Question updated Successfully", data: updatedDocument})
}catch(e){
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
}

/**
* getOne : find question from dB by Id
*/
export const getOne = model => async (req, res) => {
try {
const document = await model.findById(req.params.id)
.select("-updatedAt, -__v")
.populate("answers")
.exec();

if (!document) {
return res.status(400).send({message: `No Question found with given id: ${req.params.id}`});
}
res.status(200).send({ data: document });
} catch(e) {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
};

/**
* getMany : get all the questions from DB created by user based on userID
*/
export const getMany = model => async (req, res) => {
try{
const documents = await model.find({createdBy: req.user._id})
.select("-updatedAt, -__v")
.lean()
.populate("answers")
.exec();

res.status(200).json({ message: `All question asked by: ${req.user.firstName} ${req.user.lastName}`, data: documents});
}catch(e){
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
}

/**
* deleteOne: delete question based on userId and QuestionId
*/
export const deleteOne = model => async (req, res) => {
try{
const removed = await model.findOneAndRemove({
createdBy: req.user._id,
_id: req.params.id
})
.select("-updatedAt, -__v");

if(!removed){
return res.status(400).end();
}
return res.status(200).json({ message: "Question deleted Successfully", data: removed})
}catch(e){
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', e)
res.status(400).end();
}
}

/**
* list of operation
*/
export const crudOperationList = model => ({
createOuestion: createOne(model),
findQuestionById: getOne(model),
findQuestion: getMany(model),
updateQuestion: updateOne(model),
deleteQuestion: deleteOne(model)
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import mongoose from "mongoose";
import options from "../config/index.js";

/**
* connectDb: connect to db
*/
const connectDb = (url = options.dbUrl, opts = {}) => {
mongoose.connect(url, { ...opts, useNewUrlParser: true });
mongoose.connection
.once("open", () => {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', "Database Connected....");
})
.on("error", error => {
console.log('[' + new Date().toLocaleString('en-US', {timeZone: 'Asia/Kolkata'}) + '] ', error);
});
};

export default connectDb;
6,879 changes: 6,879 additions & 0 deletions stackOverflow-nodeJs-App-SwiggyIPP-batch_2-Prikshit_Rana/package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "stackoverflow.com",
"version": "0.0.1-dev-b254f78",
"description": "The application serves as a platform for users to ask and answer questions.",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app/app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"eslint": "^8.6.0",
"express": "^4.17.2",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"mongodb": "^4.3.0",
"mongoose": "^6.1.5",
"morgan": "^1.10.0",
"prettier": "^1.9.1",
"prettier-eslint": "^8.2.2",
"run": "^1.4.0",
"swagger-jsdoc": "^6.1.0",
"swagger-ui-express": "^4.3.0",
"validator": "^13.7.0"
},
"type": "module"
}