Upload existing files for Task Manager API/Backend
parent
ff05eb4aef
commit
9848c9d71c
@ -0,0 +1,349 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const { mongoose } = require('./db/mongoose');
|
||||
|
||||
/* MIDDLEWARE */
|
||||
// Load Middleware
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// CORS Headers
|
||||
app.use(function (req, res, next) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE");
|
||||
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, x-access-token, x-refresh-token, _id");
|
||||
res.header('Access-Control-Expose-Headers', 'x-access-token, x-refresh-token');
|
||||
next();
|
||||
});
|
||||
|
||||
// Check whether the request has a valid JWT access token
|
||||
let authenticate = (req, res, next) => {
|
||||
let token = req.header('x-access-token');
|
||||
|
||||
// Verify the JWT
|
||||
jwt.verify(token, User.getJWTSecret(), (err, decoded) => {
|
||||
if (err) {
|
||||
// Error occurred
|
||||
// JWT is invalid - *** DO NOT AUTHENTICATE ***
|
||||
res.status(401).send(err);
|
||||
} else {
|
||||
// JWT is valid
|
||||
req.user_id = decoded._id;
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Verify Refresh Token Middleware (which will be verifying the session)
|
||||
let verifySession = (req, res, next) => {
|
||||
// grab the refresh token from the request header
|
||||
let refreshToken = req.header('x-refresh-token');
|
||||
|
||||
// grab the _id from the request header
|
||||
let _id = req.header('_id');
|
||||
|
||||
User.findByIdAndToken(_id, refreshToken).then((user) => {
|
||||
if (!user) {
|
||||
// user couldn't be found
|
||||
return Promise.reject({
|
||||
'error': 'User not found. Make sure that the Refresh Token and User Id are valid'
|
||||
});
|
||||
}
|
||||
|
||||
// if the code reaches here - the user was found
|
||||
// therefore the refresh token exists in the database - but we still have to check if it has expired or not
|
||||
|
||||
req.user_id = user._id;
|
||||
req.userObject = user;
|
||||
req.refreshToken = refreshToken;
|
||||
|
||||
let isSessionValid = false;
|
||||
|
||||
user.sessions.forEach((session) => {
|
||||
if (session.token === refreshToken) {
|
||||
// check if the session has expired
|
||||
if (User.hasRefreshTokenExpired(session.expiresAt) === false) {
|
||||
// refresh token has not expired
|
||||
isSessionValid = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isSessionValid) {
|
||||
// the session is VALID - call next() to continue with processing this web request
|
||||
next();
|
||||
} else {
|
||||
// the session is not valid
|
||||
return Promise.reject({
|
||||
'error': 'Refresh Token has Expired or the Session is Invalid'
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
res.status(401).send(e);
|
||||
});
|
||||
}
|
||||
|
||||
/* MONGOOSE MODELS */
|
||||
const { List, Task, User } = require('./db/models');
|
||||
|
||||
/* ROUTE HANDLERS */
|
||||
|
||||
/* LIST ROUTES */
|
||||
|
||||
/**
|
||||
* GET /lists
|
||||
* Purpose: Get all lists
|
||||
*/
|
||||
app.get('/lists', authenticate, (req, res) => {
|
||||
// Return array of lists in the database that belong to the authenticate user
|
||||
List.find({
|
||||
_userId: req.user_id
|
||||
}).then((lists) => {
|
||||
res.send(lists);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /lists
|
||||
* Purpose: Create a list
|
||||
*/
|
||||
app.post('/lists', authenticate, (req, res) => {
|
||||
// Create new list & return new list back to the user (including id)
|
||||
// List information will be passed in through the JSON request body
|
||||
let title = req.body.title;
|
||||
|
||||
let newList = new List({
|
||||
title,
|
||||
_userId: req.user_id
|
||||
});
|
||||
newList.save().then((listDocument) => {
|
||||
// Full List Document Is Returned
|
||||
res.send(listDocument);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /lists/:id
|
||||
* Purpose: Update specified list
|
||||
*/
|
||||
app.patch('/lists/:id', authenticate, (req, res) => {
|
||||
// Update the specified list with the new values specified in the JSON request body
|
||||
List.findOneAndUpdate({ _id: req.params.id, _userId: req.user_id }, {
|
||||
$set: req.body
|
||||
}).then(() => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /lists/:id
|
||||
* Purpose: Delete specified list
|
||||
*/
|
||||
app.delete('/lists/:id', authenticate, (req, res) => {
|
||||
// Delete specified list
|
||||
List.findOneAndRemove({
|
||||
_id: req.params.id,
|
||||
_userId: req.user_id
|
||||
}).then((removedListDocument) => {
|
||||
res.send(removedListDocument);
|
||||
|
||||
// Delete All Tasks Within Deleted List
|
||||
deleteTasksFromList(removedListDocument._id);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /lists/:listId/tasks
|
||||
* Purpose: Get all tasks within specific list
|
||||
*/
|
||||
app.get('/lists/:listId/tasks', authenticate, (req, res) => {
|
||||
// Return tasks that belong to a specific list
|
||||
Task.find({
|
||||
_listId: req.params.listId
|
||||
}).then((tasks) => {
|
||||
res.send(tasks);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /lists/:listId/tasks/:taskId
|
||||
* Purpose: Retrieve specific task from specific list
|
||||
*/
|
||||
app.get('/lists/:listId/tasks/:taskId', authenticate, (req, res) => {
|
||||
Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
_listId: req.params.listId
|
||||
}).then((task) => {
|
||||
res.send(task);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /lists/:listId/tasks
|
||||
* Purpose: Create a new task in a specific list
|
||||
*/
|
||||
app.post('/lists/:listId/tasks', authenticate, (req, res) => {
|
||||
// Create a new task in a list specified by listId
|
||||
|
||||
List.findOne({
|
||||
_id: req.params.listId,
|
||||
_userId: req.user_id
|
||||
}).then((list) => {
|
||||
return !!list;
|
||||
}).then((canCreateTask) => {
|
||||
if (canCreateTask) {
|
||||
let newTask = new Task({
|
||||
title: req.body.title,
|
||||
_listId: req.params.listId
|
||||
});
|
||||
newTask.save().then((newTaskDocument) => {
|
||||
res.send(newTaskDocument);
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /lists/:listId/tasks/:taskId
|
||||
* Purpose: Update an existing task
|
||||
*/
|
||||
app.patch('/lists/:listId/tasks/:taskId', authenticate, (req, res) => {
|
||||
// Update an existing task specified by taskId
|
||||
|
||||
List.findOne({
|
||||
_id: req.params.listId,
|
||||
_userId: req.user_id
|
||||
}).then((list) => {
|
||||
return !!list;
|
||||
}).then((canUpdateTask) => {
|
||||
if (canUpdateTask) {
|
||||
Task.findOneAndUpdate({
|
||||
_id: req.params.taskId,
|
||||
_listId: req.params.listId
|
||||
}, {
|
||||
$set: req.body
|
||||
}).then(() => {
|
||||
res.send({ message: "Updated Successfully" });
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /lists/:listId/tasks/:taskId
|
||||
* Purpose: Delete a task
|
||||
*/
|
||||
app.delete('/lists/:listId/tasks/:taskId', authenticate, (req, res) => {
|
||||
// Delete a task specified by taskId
|
||||
|
||||
List.findOne({
|
||||
_id: req.params.listId,
|
||||
_userId: req.user_id
|
||||
}).then((list) => {
|
||||
return !!list;
|
||||
}).then((canDeleteTask) => {
|
||||
if (canDeleteTask) {
|
||||
Task.findOneAndRemove({
|
||||
_id: req.params.taskId,
|
||||
_listId: req.params.listId
|
||||
}).then((removedTaskDocument) => {
|
||||
res.send(removedTaskDocument);
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* USER ROUTES */
|
||||
|
||||
/**
|
||||
* POST /users
|
||||
* Purpose: Sign up
|
||||
*/
|
||||
app.post('/users', (req, res) => {
|
||||
// User sign up
|
||||
let body = req.body;
|
||||
let newUser = new User(body);
|
||||
|
||||
newUser.save().then(() => {
|
||||
return newUser.createSession();
|
||||
}).then((refreshToken) => {
|
||||
// Session created successfully - refreshToken returned.
|
||||
// now we geneate an access auth token for the user
|
||||
|
||||
return newUser.generateAccessAuthToken().then((accessToken) => {
|
||||
// access auth token generated successfully, now we return an object containing the auth tokens
|
||||
return { accessToken, refreshToken }
|
||||
});
|
||||
}).then((authTokens) => {
|
||||
// Now we construct and send the response to the user with their auth tokens in the header and the user object in the body
|
||||
res
|
||||
.header('x-refresh-token', authTokens.refreshToken)
|
||||
.header('x-access-token', authTokens.accessToken)
|
||||
.send(newUser);
|
||||
}).catch((e) => {
|
||||
res.status(400).send(e);
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* POST /users/login
|
||||
* Purpose: Login
|
||||
*/
|
||||
app.post('/users/login', (req, res) => {
|
||||
let email = req.body.email;
|
||||
let password = req.body.password;
|
||||
|
||||
User.findByCredentials(email, password).then((user) => {
|
||||
return user.createSession().then((refreshToken) => {
|
||||
// Session created successfully - refreshToken returned.
|
||||
// now we geneate an access auth token for the user
|
||||
|
||||
return user.generateAccessAuthToken().then((accessToken) => {
|
||||
// access auth token generated successfully, now we return an object containing the auth tokens
|
||||
return { accessToken, refreshToken }
|
||||
});
|
||||
}).then((authTokens) => {
|
||||
// Now we construct and send the response to the user with their auth tokens in the header and the user object in the body
|
||||
res
|
||||
.header('x-refresh-token', authTokens.refreshToken)
|
||||
.header('x-access-token', authTokens.accessToken)
|
||||
.send(user);
|
||||
})
|
||||
}).catch((e) => {
|
||||
res.status(400).send(e);
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /users/me/access-token
|
||||
* Purpose: generates and returns an access token
|
||||
*/
|
||||
app.get('/users/me/access-token', verifySession, (req, res) => {
|
||||
// we know that the user/caller is authenticated and we have the user_id and user object available to us
|
||||
req.userObject.generateAccessAuthToken().then((accessToken) => {
|
||||
res.header('x-access-token', accessToken).send({ accessToken });
|
||||
}).catch((e) => {
|
||||
res.status(400).send(e);
|
||||
});
|
||||
})
|
||||
|
||||
/* HELPER METHODS */
|
||||
let deleteTasksFromList = (_listId) => {
|
||||
Task.deleteMany({
|
||||
_listId
|
||||
}).then(() => {
|
||||
console.log("Tasks from " + _listId + " were deleted!");
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
app.listen(3000, () => {
|
||||
console.log("Server is listening on port 3000")
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
const { List } = require('./list')
|
||||
const { Task } = require('./task')
|
||||
const { User } = require('./user')
|
||||
|
||||
module.exports = {
|
||||
List,
|
||||
Task,
|
||||
User
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const ListSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
trim: true
|
||||
},
|
||||
// With Auth
|
||||
_userId: {
|
||||
type: mongoose.Types.ObjectId,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const List = mongoose.model('List', ListSchema);
|
||||
|
||||
module.exports = { List };
|
@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const TaskSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
trim: true
|
||||
},
|
||||
_listId: {
|
||||
type: mongoose.Types.ObjectId,
|
||||
required: true
|
||||
},
|
||||
completed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const Task = mongoose.model('Task', TaskSchema);
|
||||
|
||||
module.exports = { Task };
|
@ -0,0 +1,163 @@
|
||||
const mongoose = require('mongoose');
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcryptjs');
|
||||
require('dotenv').config();
|
||||
|
||||
// JWT Secret
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
|
||||
const UserSchema = new mongoose.Schema({
|
||||
email: {
|
||||
type: String,
|
||||
minlength: 1,
|
||||
required: true,
|
||||
trim: true,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 8
|
||||
},
|
||||
sessions: [{
|
||||
token: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// Instance Methods
|
||||
UserSchema.methods.toJSON = function() {
|
||||
const user = this;
|
||||
const userObject = user.toObject();
|
||||
|
||||
// return the document except the password and sessions (these shouldn't be made available)
|
||||
return _.omit(userObject, ['password', 'sessions']);
|
||||
}
|
||||
|
||||
UserSchema.methods.generateAccessAuthToken = function() {
|
||||
const user = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
// Create JWT (JSON Web Token)
|
||||
jwt.sign({ _id: user._id.toHexString() }, jwtSecret, { expiresIn: "15m" }, (err, token) => {
|
||||
if (!err) {
|
||||
resolve(token);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
UserSchema.methods.generateRefreshAuthToken = function() {
|
||||
return new Promise((resolve) => {
|
||||
crypto.randomBytes(64, (err, buffer) => {
|
||||
if (!err) {
|
||||
let token = buffer.toString('hex');
|
||||
return resolve(token);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
UserSchema.methods.createSession = function() {
|
||||
let user = this;
|
||||
|
||||
return user.generateRefreshAuthToken().then((refreshToken) => {
|
||||
return saveSessionToDatabase(user, refreshToken);
|
||||
}).then((refreshToken) => {
|
||||
return refreshToken;
|
||||
}).catch((e) => {
|
||||
return Promise.reject("Failed to Save Session To Database \n" + e);
|
||||
});
|
||||
}
|
||||
|
||||
/* MODEL METHODS (static methods) */
|
||||
UserSchema.statics.getJWTSecret = () => {
|
||||
return jwtSecret;
|
||||
}
|
||||
|
||||
UserSchema.statics.findByIdAndToken = function (_id, token) {
|
||||
// finds user by id and token
|
||||
// used in auth middleware (verifySession)
|
||||
const User = this;
|
||||
|
||||
return User.findOne({
|
||||
_id,
|
||||
'sessions.token': token
|
||||
});
|
||||
}
|
||||
|
||||
UserSchema.statics.findByCredentials = function (email, password) {
|
||||
let User = this;
|
||||
return User.findOne({ email }).then((user) => {
|
||||
if (!user) return Promise.reject();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(password, user.password, (err, res) => {
|
||||
if (res) {
|
||||
resolve(user);
|
||||
}
|
||||
else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
UserSchema.statics.hasRefreshTokenExpired = (expiresAt) => {
|
||||
let secondsSinceEpoch = Date.now() / 1000;
|
||||
return expiresAt <= secondsSinceEpoch;
|
||||
}
|
||||
|
||||
/* MIDDLEWARE */
|
||||
// Before a user document is saved, this code runs
|
||||
UserSchema.pre('save', function (next) {
|
||||
let user = this;
|
||||
let costFactor = 10;
|
||||
|
||||
if (user.isModified('password')) {
|
||||
// if the password field has been edited/changed then run this code.
|
||||
// Generate salt and hash password
|
||||
bcrypt.genSalt(costFactor, (err, salt) => {
|
||||
bcrypt.hash(user.password, salt, (err, hash) => {
|
||||
user.password = hash;
|
||||
next();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
/* HELPER METHODS */
|
||||
let saveSessionToDatabase = (user, refreshToken) => {
|
||||
return new Promise((resolve) => {
|
||||
let expiresAt = generateRefreshTokenExpiryTime();
|
||||
user.sessions.push({ 'token': refreshToken, expiresAt });
|
||||
|
||||
user.save().then(() => {
|
||||
return resolve(refreshToken);
|
||||
}).catch((e) => {
|
||||
this.reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let generateRefreshTokenExpiryTime = () => {
|
||||
let daysUntilExpire = "10";
|
||||
let secondsUntilExpire = ((daysUntilExpire * 24) * 60) * 60;
|
||||
return ((Date.now() / 1000) + secondsUntilExpire);
|
||||
}
|
||||
|
||||
const User = mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = { User }
|
@ -0,0 +1,26 @@
|
||||
// Handle MongoDB connection logic
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
require('dotenv').config();
|
||||
|
||||
mongoose.Promise = global.Promise;
|
||||
|
||||
const username = process.env.MONGO_USERNAME;
|
||||
const password = process.env.MONGO_PASS;
|
||||
const host = process.env.SERVER_HOST;
|
||||
const port = process.env.SERVER_PORT;
|
||||
|
||||
mongoose.connect(`mongodb://${username}:${password}@${host}:${port}/TaskManager?authSource=admin`, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
|
||||
console.log("Connection Successful");
|
||||
}).catch((exception) => {
|
||||
console.log("Connection Error");
|
||||
console.log(exception);
|
||||
});
|
||||
|
||||
// Prevent Deprecation Warnings
|
||||
mongoose.set('useCreateIndex', true);
|
||||
mongoose.set('useFindAndModify', false);
|
||||
|
||||
module.exports = {
|
||||
mongoose
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Goudham Suresh",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^5.12.14",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue