-
A user should be able to Log In & SignUp using a username / password
-
A user should be able to post a thought anonymously & non-anonymously
-
A user should be able to reply to a thought anonymously & non-anonymously
-
A user should be able to delete their thoughts & replies (anonymous as well as non-anonymous)
-
Any other user shouldn't able to identify who posted what anonymously
-
A user should be able to list all the thoughts posted
-
A user should be able to list all thoughts posted by them and any other users.
Weโve got three collections, so far:
thoughts & replies collection is linked to the users table using the username.
Both, thought and replies have a status, which is one of the following -
- PUBLISHED
- DRAFT
- REMOVED
- DELETED
Anonymity is maintained by maintaining both userId & username in the database. There might be other ways but if a user has anonymous = true - the user won't have his username stored in thought or even replies. While, if a user has anonymous = false, he will also have username stored in database.
An ACID Transaction has also been implemented in the Delete Thought API. Check for API docs for implementation details.
Some internals on indexing of MongoDB:
- In MongoDB the indexes come built in with the _id (the created at time can also be derived from them) so in each collections weโll have that _id. Also, this _id is always unique.
- Other than this we can have unique indexes (MongoDB also supports those) on username field in users collection.
โจ MongoDB supports many many indexes. You can use MongoDB Compass to get an awesome view of Explain Plan to see if the indexes you intended to use are being used properly or not in your queries.
I'll be using: Node, Javascript, Express & MongoDB for the project.
In NodeJS you can import packages using npm:
How?
- npm init -y
- Initialized package.json & all the multiple options yes (due to -y)
- npm install <package_name>
- Once package.json is initialized - this command can be used throughout the project for importing new projects.
To run this project:
node src/app/api/index.js
Before running you need to have a .env file. Check for required environment variables in the src/config.js file.
Although, for production purposes & deploying it on a server. I'd use something like AWS parameter store and for running the app I'd use a process manager (pm2).
Security Related Packages:
๐ฆ joi
- Using this for schema validation
- It is used in utils/joi/ folder & used to validate incoming schema.
- List thought by Specific User API
๐ฆ bcryptjs
- Encrypting passwords & checking them when the user is logging in. Weโll be using the async method in this package.
๐ฆ jsonwebtoken
- For Signing & verifying access tokens. Weโre keeping one login - i.e. - one access token stored in the database.
Core Packages (Database & Routing):
๐ฆ express
- framework for providing routing in a simple, subtle way (creating REST APIs)
๐ฆ mongodb
- Iโll be using the MongoDB driver instead of an ORM.
- Will be writing native queries for mongo.
Miscellaneous Packages:
๐ฆ dotenv
- Used for reading environment variables through multiple environments.
- This package is used in config.js and all the environment variables are distributed through that file.
๐ฆ express-async-errors
- This is a hack for solving errors which will get uncaught if you have asynchronous functions. If you donโt use this. Express wonโt detect async errors and the request would timeout. Itโs pretty useful.
Itโs a middleware for handling the errors and giving a uniform error response.
Errors that are uncaught would be of these types:
- Database Error (500)
- Not Authenticated Error (401)
- Bad Request Error (400)
- Service Error (500)
- Not Found Error (404)
All of these have similar properties as these are custom errors that I have made in the codebase - checkout the src/app/errors folder.
This will catch the error, set status code & send a uniform response.
In case of uncaught error - it will send a response which sends response (and we can add alarms there)
A usual error response:
{
"route": "DELETE /reply",
"message": "Bad Params",
"statusCode": 400,
"error": "Bad Request",
"type": "BadRequestError"
}
This middleware is run before accessing OAuth 2.0 enabled request as they are protected & require a user to be logged in.
This function also has a getUser boolean which someone can use if he wants a user in the middleware. Iโve used it to get userId in the current project - could also have attached it to the payload while signing jwt token.
Steps that happen in auth middleware, otherwise a 401 is thrown:
- Check if token present in the request.
- Verify token, extract username.
- Two cases now, depending on getUser:
- If true, extract username from token & fetch the user. After that set it to req.user.
- If false, extract username and set it to req.username.
POST /auth/signup
Authorization not required.
// Body raw (json)
// @required
username: - should be minimum of 3 characters & maximum 25 characters
- only lowercase alphabets & numericals allowed
- ex: dwight12, yash20, jimpam420
password: - should contain a maximum of 100 characters & minimum of 8 characters
- ex: yash123456789
email: - should be a valid email
- ex: [email protected]
Example of Request: Body Raw (JSON)
{
"username": "yash123",
"password": "Default@123",
"email": "[email protected]"
}
Example of successful response:
{
"message": "user created",
"id": "6299f058cd0adfd0eac06686",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InTY1NDI1NTcwNH0.eo6kPn38msOoZV6mOtF0LhzTk2pn_R8"
}
Implementation Steps:
- Do schema validation of request using joi.
- Hash the password & sign access token (jsonwebtoken) for saving in database.
- Save the details of user in Database (check database designs)
- Return success.
POST /auth/login
Authorization not required.
// Body raw (json)
// @required
username: - should be minimum of 3 characters & maximum 25 characters
- only lowercase alphabets & numericals allowed
- ex: dwight12, yash20, jimpam420
password: - should contain a maximum of 100 characters & minimum of 8 characters
- ex: yash123456789
Example of Request: Body Raw (JSON)
{
"username": "yash123",
"password": "Default@123",
}
Example of successful response:
{
"user": {
"_id": "62990b42c5c7eefcdac0c130",
"username": "yashdixit123",
"email": "[email protected]",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inlhc2hkaXhpdDEyMyIsImlhdCI6MTY1NDQyMTcxNn0.hreNIh_vbQDKws4U2Hcs1PENC0eHKuGiim7DAH4X9NE",
"ban": false,
"verified": false
}
}
Implementation Steps:
- Do schema validation of request using joi.
- Check for user in Database & compare hashed password (using bcrypt)
- Sign new access token, and save it in database.
- Return response, delete hashed password.
POST /thought/
OAuth 2.0
// Body raw (json)
// @required
thought: - should be minimum of 5 characters & maximum of 350.
- ex: "I am not feeling well today."
anonymous: - should be a boolean
- required
- ex: false, true
Example of Request: Body Raw (JSON):
{
"thought": "I am not feeling too well today.",
"anonymous": false
}
Example of successful response:
{
"message": "thought created",
"id": "629b2032ea6ded56374b9c70"
}
POST /reply/
OAuth 2.0
// Body raw (json)
// @required
reply: - should be minimum of 5 characters & maximum of 350.
- ex: "Things get better."
anonymous: - should be a boolean
- required
- ex: false, true
thoughtId: - must be a valid thoughtId present in the database.
Example of Request: Body Raw (JSON):
{
"thoughtId": "629b2032ea6ded56374b9c70",
"reply": "Take care. Things get better with time.",
"anonymous": false
}
Example of successful response:
{
"message": "success",
"id": "629b2032ea6ded56374b9c70"
}
Implementation Steps:
-
Go through Authentication middleware.
-
Do schema validation of request using joi.
-
If anonymous:
- Donโt save username in database, only store userId.
If not anonymous:
- Save username along with userId in the database.
Also save thoughtId.
-
Return response.
GET /thought/
OAuth 2.0
Fetches all the thoughts that have been posted.
This query has pagination implemented - you can use limit & skip filters. Although, by default it limits to 10 & skips 0.
// Optional
// Body raw (json)
limit: Number. Should be greater than 0.
skip: Number. Should be greater than or equal to 0.
Example of successful response:
{
"thoughts": [
{
"_id": "629b2032ea6ded56374b9c70",
"thought": "Feeling great after grabbing job opportunity.",
"userId": "62990b42c5c7eefcdac0c130",
"anonymous": false,
"status": "PUBLISHED",
"username": "yashdixit123"
}
]
}
Implementation Steps:
- Go through Authentication middleware.
- Verify if limit & skip are valid. If they are undefined use limit as 10 & skip as 0.
- Query database for thoughts - no filter. And then apply limit and skip.
- Return response.
GET /thought/id
OAuth 2.0
Fetches thought using a thoughtId. Also returns the replies of teh corresponding thought.
// Query paramters
// @required
thoughtId: - must be a valid thoughtId present in the database.
Example of Request:
Query:
{
"thoughtId": "629b2032ea6ded56374b9c70"
}
Example of successful response:
{
"thoughts": {
"_id": "629b2032ea6ded56374b9c70",
"thought": "I'm not feeling too good.",
"userId": "62990b42c5c7eefcdac0c130",
"anonymous": false,
"status": "PUBLISHED",
"username": "yashdixit123",
"replies": [
{
"_id": "629b20afea6ded56374b9c71",
"thoughtId": "629b2032ea6ded56374b9c70",
"reply": "keep going, things get well with time. ",
"userId": "629b2010ea6ded56374b9c6f",
"anonymous": false,
"status": "PUBLISHED",
"username": "yash12345"
},
{
"_id": "629b20e0ea6ded56374b9c72",
"thoughtId": "629b2032ea6ded56374b9c70",
"reply": "take care.",
"userId": "629b2010ea6ded56374b9c6f",
"anonymous": false,
"status": "PUBLISHED",
"username": "yash12345"
},
{
"_id": "629b215eea6ded56374b9c73",
"thoughtId": "629b2032ea6ded56374b9c70",
"reply": "thank you!",
"userId": "62990b42c5c7eefcdac0c130",
"anonymous": false,
"status": "PUBLISHED",
"username": "yashdixit123"
}
]
}
}
Implementation Steps:
-
Go through Authentication middleware.
-
Do schema validation of request.
-
Do the aggregation query for getting replies which correspond to the thoughtId.
- This gets done by using aggregation pipeline in mongodb:
- First use $match (for getting specific thought) on thoughts collections.
- Then use $lookup (same as JOIN) and join on replies table.
- This gets done by using aggregation pipeline in mongodb:
-
Return response.
GET /thought/self
OAuth 2.0
No Parameters Required
Fetches all your own thoughts - be it anonymous and non-anonymous (can only be accessed by you)
Example of successful response:
{
"thoughts": [
{
"_id": "629b2032ea6ded56374b9c70",
"thought": "Feeling great after grabbing job opportunity.",
"userId": "62990b42c5c7eefcdac0c130",
"anonymous": false,
"status": "PUBLISHED",
"username": "yashdixit123"
}
]
}
Implementation Steps:
- Go through Authentication middleware.
- Get UserId from authentication middlewareโs passed on request.
- Query database for thoughts corresponding to specific userId.
- Return response.
GET /thought/username
OAuth 2.0
Query parameters: username (required)
Fetches all the non-anonymous thoughts for a particular username.
Example of successful response:
{
"thoughts": [
{
"_id": "629b2032ea6ded56374b9c70",
"thought": "Feeling great after grabbing job opportunity.",
"userId": "62990b42c5c7eefcdac0c130",
"anonymous": false,
"status": "PUBLISHED",
"username": "yashdixit123"
}
]
}
Implementation Steps:
- Go through Authentication middleware.
- Verify if username is valid & proceed on with query.
- Query database for thoughts corresponding to specific username.
- Return response.
DELETE /thought/
OAuth 2.0
Query parameters: thoughtId (required and should belong to user)
Deletes the thought & its replies with given thought Id - which should belong to the user.
Expected response in case everything is correct:
{
"message": "success"
}
Implementation Steps:
- Go through Authentication middleware.
- Check if thoughtId is a valid string.
- Query database for deleting: This includes two steps, and is a ACID transaction:
- Delete One on thoughts collection (send userId too, to ensure it belongs to him)
- If this fails or doesnโt delete, ROLLBACK & return error.
- Delete Many on replies collection (with all the replies which have same thoughtId)
- If this fails - ROLLBACK & return error.
- Delete One on thoughts collection (send userId too, to ensure it belongs to him)
- Return response.
DELETE /reply/
OAuth 2.0
Query parameters: replyId (required and should belong to user)
Deletes the reply with replyId - which should belong to the user.
Expected response in case everything is correct:
{
"message": "success"
}
Implementation Steps:
- Go through Authentication middleware.
- Check if replyId is a valid string.
- Query database for deleting includes querying with replyId & userId to ensure that reply belongs to user.
- Return response.