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

feature/FirstAssignment #105

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Create a .env file and add "JWT_SECRET" as variable name
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.env
157 changes: 157 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# FullStack Assignment

## Usage

1. Start the server: `npm start`
2. Open your browser and navigate to `http://localhost:3001`
## Folder Structure

The project follows a modular architecture, where the code is organized into separate folders for better maintainability and scalability. Here's a brief description of each folder:

- `controllers`: Contains the controllers for handling incoming requests, performing necessary actions, and sending the response back.
- `middleware`: Contains the middleware functions to handle common tasks such as authentication, input validation, error handling, etc.
- `models`: Contains the models for the data store used in the application.
- `routes`: Contains the route handlers for mapping HTTP requests to the corresponding controller functions.

## API Endpoints

### Authentication

- `/signup` (POST): Allows users to sign up for the platform
- `/login` (POST): Allows registered users to login to the platform

### Questions

- `/questions` (GET): Returns a list of all available questions

### Submissions

- `/submissions/:questionId` (GET): Returns a list of submissions for a particular question
- `/submissions/:questionId` (POST): Allows logged in users to submit a solution for a particular question
### Admin Routes
Implemented separate routes, controllers, middleware, and models for admin functionality, for separation of concerns
- `/admin/signup` (POST): Allows admin to sign up for the platform
- `/admin/login` (POST): Allows registered admin to login to the platform
- `/admin/question` (POST): Allows registered admin to add question.
## Object Types

### Question Type

| Field | Type | Description |
| ----------- | ------ | ------------------------------------ |
| id | Number | The unique ID of the question |
| title | String | The title of the question |
| description | String | The description of the question |
| testCases | Array | An array of test cases for the question |

The `testCases` array contains objects with the following fields:

| Field | Type | Description |
| ------ | ------ | ------------------------------- |
| input | String | The input for the test case |
| output | String | The expected output for the test case |

```json
{
"id": 1,
"title": "Two states",
"description": "Given an array, return the maximum of the array?",
"testCases": [
{
"input": "[1,2,3,4,5]",
"output": "5"
},
{
"input": "[-10,-5,0,5,10]",
"output": "10"
},
{
"input": "[3,7,2,8,4]",
"output": "8"
}
]
}
```

### Submission Type

| Field | Type | Description |
| ------------ | ------ | ---------------------------------------------- |
| questionID | String | The ID of the question the submission relates to |
| submissions | Array | An array of submissions for the question |

The `submissions` array contains objects with the following fields:

| Field | Type | Description |
| ------- | ------ | ---------------------------------- |
| userId | String | The ID of the user making the submission |
| code | String | The code for the submission |
| status | String | The status of the submission |

```json
{
"questionID": "1",
"submissions": [
{
"userId": "123",
"code": "console.log('hello world')",
"status": "accept"
},
{
"userId": "456",
"code": "console.log('hello')",
"status": "reject"
}
]
}
```


## Input Validation and Sanitisation

To ensure data integrity and prevent security vulnerabilities, this application uses input validation and sanitization. We have used the express-validator library to add validation and sanitization middleware for the following routes:

- `/signup`: Validates and sanitizes the user's email and password inputs.
- `/login`: Validates and sanitizes the user's email and password inputs.
- `/admin/question`: Validates and sanitizes the admin's question inputs.
- `/submissions/question`: Validates and santizes the code input.

Example:
```js
exports.createQuestionValidator = [
body("title")
.trim()
.isLength({ min: 1, max: 100 })
.escape()
.withMessage("Title is required"),
body("description")
.trim()
.isLength({ min: 1, max: 500 })
.escape()
.withMessage("Description is required"),
body("testCases")
.isArray()
.notEmpty()
.withMessage("Test cases must be an array with at least one element"),
body("testCases.*.input")
.trim()
.isLength({ min: 1, max: 100 })
.escape()
.withMessage("Test case must have input and output fields"),
body("testCases.*.output")
.trim()
.isLength({ min: 1, max: 100 })
.escape()
.withMessage("Test case must have input and output fields"),
];
```

## Authentication
The application uses `JSON Web Tokens (JWT)` for authentication. When a user logs in, a JWT is generated and sent to the user with a cookie. For subsequent requests, the JWT is included in the cookie, which is then verified on the server side to authenticate the user.

It's important to note that while the authentication process is similar for both users and admins, admin authentication requires a separate set of routes, controllers, middleware, and models for separation of concerns.

To protect routes, the application uses an `authMiddleware` which checks if the user is authenticated and authorized to access the route. The middleware checks for the presence of the JWT cookie in the request headers. If the cookie is present and the JWT is verified, the user is considered authenticated and authorized to access the route.

In order to get the cookie in the `authMiddleware`, the application uses the `cookie-parser `middleware which parses the cookie from the request headers and makes it available to the authMiddleware for authentication and authorization checks.

81 changes: 81 additions & 0 deletions controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { validationResult } = require("express-validator");
const ADMINS = require("../models/Admin");
const bcrypt = require("bcrypt");
const { v1 } = require("uuid");
const jwt = require("jsonwebtoken");

exports.signup = async (req, res) => {
// Check for validation errors
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

// Check if admin already exists
const existingAdmin = ADMINS.find((admin) => admin.email === req.body.email);
if (existingAdmin) {
return res
.status(409)
.json({ message: "Admin with this email already exists" });
}

try {
// Hash password
const hashedPassword = await bcrypt.hash(req.body.password, 10);

// Add admin to ADMINS array
ADMINS.push({
id: v1(), // add unique id
email: req.body.email,
password: hashedPassword,
});

// Send success response
res.sendStatus(201);
} catch (error) {
// Handle errors
console.error(error);
res.status(500).json({ message: "Internal server error" });
}
};

exports.login = async (req, res) => {
// Check for validation errors
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;

try {
// Check if admin exists in ADMINS array
const admin = await ADMINS.find((val) => val.email === email);
if (!admin) {
return res.status(401).json({ msg: "Invalid email or password" });
}

// Check if password is correct
const isMatch = await bcrypt.compare(password, admin.password);
if (!isMatch) {
return res.status(401).json({ msg: "Invalid email or password" });
}

// Generate JSON Web Token (JWT)
const payload = {
admin: {
id: admin.id,
email: admin.email,
},
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
res.cookie("token", token, { httpOnly: true, maxAge: 3600 });
res.sendStatus(200);
} catch (err) {
console.error(err);
res.status(500).send("Server Error");
}
};
81 changes: 81 additions & 0 deletions controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const { validationResult } = require("express-validator");
const USERS = require("../models/User");
const bcrypt = require("bcrypt");
const { v1 } = require("uuid");
const jwt = require("jsonwebtoken");

exports.signup = async (req, res) => {
// Check for validation errors
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

// Check if user already exists
const existingUser = USERS.find((user) => user.email === req.body.email);
if (existingUser) {
return res
.status(409)
.json({ message: "User with this email already exists" });
}

try {
// Hash password
const hashedPassword = await bcrypt.hash(req.body.password, 10);

// Add user to USERS array
USERS.push({
id: v1(), // add unique id
email: req.body.email,
password: hashedPassword,
});

// Send success response
res.sendStatus(201);
} catch (error) {
// Handle errors
console.error(error);
res.status(500).json({ message: "Internal server error" });
}
};

exports.login = async (req, res) => {
// Check for validation errors
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { email, password } = req.body;

try {
// Check if user exists in USERS array
const user = await USERS.find((val) => val.email === email);
if (!user) {
return res.status(401).json({ msg: "Invalid email or password" });
}

// Check if password is correct
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ msg: "Invalid email or password" });
}

// Generate JSON Web Token (JWT)
const payload = {
user: {
id: user.id,
email: user.email,
},
};
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
res.cookie("token", token, { httpOnly: true, maxAge: 3600 });
res.sendStatus(200);
} catch (err) {
console.error(err);
res.status(500).send("Server Error");
}
};
38 changes: 38 additions & 0 deletions controllers/questionsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { validationResult } = require("express-validator");
const QUESTIONS = require("../models/Questions");

exports.getAllQuestions = (req, res) => {
try {
const formattedQuestions = QUESTIONS;

res.status(200).json(formattedQuestions);
} catch (err) {
return res.status(500).json({ error: "Server error" });
}
};

exports.addQuestion = (req, res) => {
// Validate and sanitize inputs using express-validator
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}

try {
const { title, description, testCases } = req.body;

const newQuestion = {
id: QUESTIONS.length + 1,
title,
description,
testCases,
};

QUESTIONS.push(newQuestion);
res
.status(201)
.json({ message: "Question added successfully!", question: newQuestion });
} catch (error) {
return res.status(500).json({ error: "Server error" });
}
};
Loading