ASSISTANT
Certainly! Below is a comprehensive guide to building a backend system for an Assignment Submission Portal using **Node.js**, **Express**, and **MongoDB** with **Mongoose**. This guide includes the project structure, implementation of required functionalities, and instructions to set up and run the system.
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Tech Stack](#tech-stack)
3. [Project Structure](#project-structure)
4. [Setup and Installation](#setup-and-installation)
5. [Environment Variables](#environment-variables)
6. [Models](#models)
- [User Model](#user-model)
- [Assignment Model](#assignment-model)
7. [Middleware](#middleware)
- [Authentication Middleware](#authentication-middleware)
- [Authorization Middleware](#authorization-middleware)
8. [Routes](#routes)
- [User Routes](#user-routes)
- [Admin Routes](#admin-routes)
9. [Validation](#validation)
10. [Error Handling](#error-handling)
11. [Running the Application](#running-the-application)
12. [API Documentation](#api-documentation)
13. [Conclusion](#conclusion)
---
## Project Overview
We will develop a backend system for an **Assignment Submission Portal** with the following capabilities:
- **Users** can register, log in, and upload assignments.
- **Admins** can register, log in, view assignments assigned to them, and accept or reject assignments.
MongoDB will serve as our database, and we will use **JWT** for authentication.
---
## Tech Stack
- **Runtime Environment:** Node.js
- **Framework:** Express.js
- **Database:** MongoDB
- **ODM:** Mongoose
- **Authentication:** JSON Web Tokens (JWT)
- **Validation:** Joi
- **Other Utilities:** bcrypt for password hashing, dotenv for environment variables, etc.
---
## Project Structure
```
assignment-portal/
│
├── controllers/
│ ├── adminController.js
│ ├── assignmentController.js
│ └── userController.js
│
├── middleware/
│ ├── auth.js
│ └── validate.js
│
├── models/
│ ├── Assignment.js
│ └── User.js
│
├── routes/
│ ├── adminRoutes.js
│ ├── assignmentRoutes.js
│ └── userRoutes.js
│
├── utils/
│ └── generateToken.js
│
├── validations/
│ ├── adminValidation.js
│ ├── assignmentValidation.js
│ └── userValidation.js
│
├── .env
├── .gitignore
├── package.json
├── package-lock.json
└── server.js
```
---
## Setup and Installation
1. **Clone the Repository:**
```bash
git clone https://github.com/your-username/assignment-portal.git
cd assignment-portal
```
2. **Initialize Node.js Project:**
```bash
npm init -y
```
3. **Install Dependencies:**
```bash
npm install express mongoose bcryptjs jsonwebtoken dotenv joi
```
4. **Install Dev Dependencies (Optional):**
```bash
npm install --save-dev nodemon
```
5. **Update `package.json` Scripts:**
```json
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
```
---
## Environment Variables
Create a `.env` file in the root directory and add the following variables:
```
PORT=5000
MONGODB_URI=your_mongodb_connection_string
JWT_SECRET=your_jwt_secret_key
```
*Replace `your_mongodb_connection_string` and `your_jwt_secret_key` with your actual MongoDB URI and a strong secret key.*
---
## Models
### User Model
```javascript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please provide name'],
},
email: {
type: String,
required: [true, 'Please provide email'],
unique: true,
match: [
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
'Please provide a valid email',
],
},
password: {
type: String,
required: [true, 'Please provide password'],
minlength: 6,
select: false,
},
role: {
type: String,
enum: ['User', 'Admin'],
default: 'User',
},
}, { timestamps: true });
// Encrypt password before saving
userSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare password
userSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
```
### Assignment Model
```javascript
// models/Assignment.js
const mongoose = require('mongoose');
const assignmentSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
task: {
type: String,
required: [true, 'Please provide task details'],
},
admin: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
status: {
type: String,
enum: ['Pending', 'Accepted', 'Rejected'],
default: 'Pending',
},
}, { timestamps: true });
module.exports = mongoose.model('Assignment', assignmentSchema);
```
---
## Middleware
### Authentication Middleware
This middleware verifies JWT tokens and attaches the user to the request object.
```javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const asyncHandler = require('express-async-handler');
exports.protect = asyncHandler(async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')
) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
res.status(401);
throw new Error('Not authorized, no token');
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (error) {
res.status(401);
throw new Error('Not authorized, token failed');
}
});
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
res.status(403);
throw new Error(`User role ${req.user.role} is unauthorized`);
}
next();
};
};
```
### Validation Middleware
Middleware to validate request bodies using Joi schemas.
```javascript
// middleware/validate.js
const asyncHandler = require('express-async-handler');
const validate = (schema) => {
return asyncHandler(async (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
res.status(400);
throw new Error(error.details[0].message);
}
next();
});
};
module.exports = validate;
```
---
## Routes
### User Routes
Handles user registration, login, and assignment upload.
```javascript
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { registerUser, loginUser, uploadAssignment, getAdmins } = require('../controllers/userController');
const { protect, authorize } = require('../middleware/auth');
const validate = require('../middleware/validate');
const { registerSchema, loginSchema, assignmentUploadSchema } = require('../validations/userValidation');
// Register User
router.post('/register', validate(registerSchema), registerUser);
// Login User
router.post('/login', validate(loginSchema), loginUser);
// Upload Assignment
router.post('/upload', protect, authorize('User'), validate(assignmentUploadSchema), uploadAssignment);
// Get All Admins
router.get('/admins', protect, authorize('User', 'Admin'), getAdmins);
module.exports = router;
```
### Admin Routes
Handles admin registration, login, viewing assignments, and accepting/rejecting assignments.
```javascript
// routes/adminRoutes.js
const express = require('express');
const router = express.Router();
const { registerAdmin, loginAdmin, getAssignments, acceptAssignment, rejectAssignment } = require('../controllers/adminController');
const { protect, authorize } = require('../middleware/auth');
const validate = require('../middleware/validate');
const { registerSchema, loginSchema } = require('../validations/adminValidation');
// Register Admin
router.post('/register', validate(registerSchema), registerAdmin);
// Login Admin
router.post('/login', validate(loginSchema), loginAdmin);
// Get Assignments Assigned to Admin
router.get('/assignments', protect, authorize('Admin'), getAssignments);
// Accept Assignment
router.post('/assignments/:id/accept', protect, authorize('Admin'), acceptAssignment);
// Reject Assignment
router.post('/assignments/:id/reject', protect, authorize('Admin'), rejectAssignment);
module.exports = router;
```
---
## Controllers
### User Controller
Implements functionalities for users.
```javascript
// controllers/userController.js
const asyncHandler = require('express-async-handler');
const User = require('../models/User');
const Assignment = require('../models/Assignment');
const generateToken = require('../utils/generateToken');
// @desc Register new user
// @route POST /api/users/register
// @access Public
exports.registerUser = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
const userExists = await User.findOne({ email });
if (userExists) {
res.status(400);
throw new Error('User already exists');
}
const user = await User.create({
name,
email,
password,
});
if (user) {
res.status(201).json({
_id: user.id,
name: user.name,
email: user.email,
role: user.role,
token: generateToken(user._id),
});
} else {
res.status(400);
throw new Error('Invalid user data');
}
});
// @desc Authenticate a user
// @route POST /api/users/login
// @access Public
exports.loginUser = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (user && (await user.matchPassword(password))) {
res.json({
_id: user.id,
name: user.name,
email: user.email,
role: user.role,
token: generateToken(user._id),
});
} else {
res.status(401);
throw new Error('Invalid email or password');
}
});
// @desc Upload an assignment
// @route POST /api/users/upload
// @access Private (User)
exports.uploadAssignment = asyncHandler(async (req, res) => {
const { task, adminId } = req.body;
// Ensure the admin exists and is an Admin
const admin = await User.findById(adminId);
if (!admin || admin.role !== 'Admin') {
res.status(400);
throw new Error('Invalid admin specified');
}
const assignment = await Assignment.create({
userId: req.user._id,
task,
admin: adminId,
});
res.status(201).json(assignment);
});
// @desc Get all admins
// @route GET /api/users/admins
// @access Private (User & Admin)
exports.getAdmins = asyncHandler(async (req, res) => {
const admins = await User.find({ role: 'Admin' }).select('-password');
res.json(admins);
});
```
### Admin Controller
Implements functionalities for admins.
```javascript
// controllers/adminController.js
const asyncHandler = require('express-async-handler');
const User = require('../models/User');
const Assignment = require('../models/Assignment');
const generateToken = require('../utils/generateToken');
// @desc Register new admin
// @route POST /api/admins/register
// @access Public
exports.registerAdmin = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
const adminExists = await User.findOne({ email });
if (adminExists) {
res.status(400);
throw new Error('Admin already exists');
}
const admin = await User.create({
name,
email,
password,
role: 'Admin',
});
if (admin) {
res.status(201).json({
_id: admin.id,
name: admin.name,
email: admin.email,
role: admin.role,
token: generateToken(admin._id),
});
} else {
res.status(400);
throw new Error('Invalid admin data');
}
});
// @desc Authenticate an admin
// @route POST /api/admins/login
// @access Public
exports.loginAdmin = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const admin = await User.findOne({ email }).select('+password');
if (admin && (await admin.matchPassword(password)) && admin.role === 'Admin') {
res.json({
_id: admin.id,
name: admin.name,
email: admin.email,
role: admin.role,
token: generateToken(admin._id),
});
} else {
res.status(401);
throw new Error('Invalid email or password for admin');
}
});
// @desc Get assignments assigned to admin
// @route GET /api/admins/assignments
// @access Private (Admin)
exports.getAssignments = asyncHandler(async (req, res) => {
const assignments = await Assignment.find({ admin: req.user._id })
.populate('userId', 'name email')
.sort({ createdAt: -1 });
res.json(assignments);
});
// @desc Accept an assignment
// @route POST /api/admins/assignments/:id/accept
// @access Private (Admin)
exports.acceptAssignment = asyncHandler(async (req, res) => {
const assignment = await Assignment.findById(req.params.id);
if (!assignment) {
res.status(404);
throw new Error('Assignment not found');
}
if (assignment.admin.toString() !== req.user._id.toString()) {
res.status(403);
throw new Error('Not authorized to accept this assignment');
}
assignment.status = 'Accepted';
await assignment.save();
res.json({ message: 'Assignment accepted', assignment });
});
// @desc Reject an assignment
// @route POST /api/admins/assignments/:id/reject
// @access Private (Admin)
exports.rejectAssignment = asyncHandler(async (req, res) => {
const assignment = await Assignment.findById(req.params.id);
if (!assignment) {
res.status(404);
throw new Error('Assignment not found');
}
if (assignment.admin.toString() !== req.user._id.toString()) {
res.status(403);
throw new Error('Not authorized to reject this assignment');
}
assignment.status = 'Rejected';
await assignment.save();
res.json({ message: 'Assignment rejected', assignment });
});
```
---
## Validations
Using **Joi** for request body validations.
### User Validation
```javascript
// validations/userValidation.js
const Joi = require('joi');
exports.registerSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
exports.loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
});
exports.assignmentUploadSchema = Joi.object({
task: Joi.string().min(3).required(),
adminId: Joi.string().length(24).required(), // Assuming MongoDB ObjectId length
});
```
### Admin Validation
```javascript
// validations/adminValidation.js
const Joi = require('joi');
exports.registerSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
exports.loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
});
```
---
## Utility Functions
### Generate Token
Generates JWT tokens for authentication.
```javascript
// utils/generateToken.js
const jwt = require('jsonwebtoken');
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d',
});
};
module.exports = generateToken;
```
---
## Server Setup
```javascript
// server.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');
const adminRoutes = require('./routes/adminRoutes');
const { errorHandler } = require('./middleware/errorHandler');
dotenv.config();
const app = express();
// Middleware to parse JSON
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(() => console.log('MongoDB connected'))
.catch((err) => {
console.error(err);
process.exit(1);
});
// Routes
app.use('/api/users', userRoutes);
app.use('/api/admins', adminRoutes);
// Error Handler Middleware
app.use(errorHandler);
// Start Server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
```
### Error Handling Middleware
```javascript
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
// stack: process.env.NODE_ENV === 'production' ? null : err.stack,
});
};
module.exports = { errorHandler };
```
---
## Running the Application
1. **Ensure MongoDB is running and accessible.**
2. **Start the Server:**
- For development with **nodemon**:
```bash
npm run dev
```
- For production:
```bash
npm start
```
3. **Server should be running on `http://localhost:5000` or the port specified in `.env`.**
---
## API Documentation
### Base URL
```
http://localhost:5000/api
```
### User Endpoints
1. **Register User**
- **URL:** `/users/register`
- **Method:** `POST`
- **Body:**
```json
{
"name": "Soumik",
"email": "soumik@example.com",
"password": "password123"
}
```
- **Response:**
```json
{
"_id": "userId",
"name": "Soumik",
"email": "soumik@example.com",
"role": "User",
"token": "jwt_token"
}
```
2. **Login User**
- **URL:** `/users/login`
- **Method:** `POST`
- **Body:**
```json
{
"email": "soumik@example.com",
"password": "password123"
}
```
- **Response:**
```json
{
"_id": "userId",
"name": "Soumik",
"email": "soumik@example.com",
"role": "User",
"token": "jwt_token"
}
```
3. **Upload Assignment**
- **URL:** `/users/upload`
- **Method:** `POST`
- **Headers:**
```
Authorization: Bearer <jwt_token>
```
- **Body:**
```json
{
"task": "Hello World",
"adminId": "adminUserId"
}
```
- **Response:**
```json
{
"_id": "assignmentId",
"userId": "userId",
"task": "Hello World",
"admin": "adminUserId",
"status": "Pending",
"createdAt": "timestamp",
"updatedAt": "timestamp"
}
```
4. **Get All Admins**
- **URL:** `/users/admins`
- **Method:** `GET`
- **Headers:**
```
Authorization: Bearer <jwt_token>
```
- **Response:**
```json
[
{
"_id": "adminId",
"name": "Alok",
"email": "alok@example.com",
"role": "Admin",
"createdAt": "timestamp",
"updatedAt": "timestamp"
},
...
]
```
### Admin Endpoints
1. **Register Admin**
- **URL:** `/admins/register`
- **Method:** `POST`
- **Body:**
```json
{
"name": "Alok",
"email": "alok@example.com",
"password": "password123"
}
```
- **Response:**
```json
{
"_id": "adminId",
"name": "Alok",
"email": "alok@example.com",
"role": "Admin",
"token": "jwt_token"
}
```
2. **Login Admin**
- **URL:** `/admins/login`
- **Method:** `POST`
- **Body:**
```json
{
"email": "alok@example.com",
"password": "password123"
}
```
- **Response:**
```json
{
"_id": "adminId",
"name": "Alok",
"email": "alok@example.com",
"role": "Admin",
"token": "jwt_token"
}
```
3. **Get Assignments Assigned to Admin**
- **URL:** `/admins/assignments`
- **Method:** `GET`
- **Headers:**
```
Authorization: Bearer <jwt_token>
```
- **Response:**
```json
[
{
"_id": "assignmentId",
"userId": {
"_id": "userId",
"name": "Soumik",
"email": "soumik@example.com"
},
"task": "Hello World",
"admin": "adminId",
"status": "Pending",
"createdAt": "timestamp",
"updatedAt": "timestamp"
},
...
]
```
4. **Accept Assignment**
- **URL:** `/admins/assignments/:id/accept`
- **Method:** `POST`
- **Headers:**
```
Authorization: Bearer <jwt_token>
```
- **Response:**
```json
{
"message": "Assignment accepted",
"assignment": {
"_id": "assignmentId",
"userId": "userId",
"task": "Hello World",
"admin": "adminId",
"status": "Accepted",
"createdAt": "timestamp",
"updatedAt": "timestamp"
}
}
```
5. **Reject Assignment**
- **URL:** `/admins/assignments/:id/reject`
- **Method:** `POST`
- **Headers:**
```
Authorization: Bearer <jwt_token>
```
- **Response:**
```json
{
"message": "Assignment rejected",
"assignment": {
"_id": "assignmentId",
"userId": "userId",
"task": "Hello World",
"admin": "adminId",
"status": "Rejected",
"createdAt": "timestamp",
"updatedAt": "timestamp"
}
}
```
---
## Conclusion
This backend system is structured to provide a clear separation between user and admin functionalities while ensuring secure authentication and authorization mechanisms. The use of **JWT** ensures that protected routes are only accessible to authenticated users, and roles (`User` and `Admin`) are used to authorize specific actions.
The project is modular, making it easy to maintain and extend. Proper validations and error handling are implemented to ensure robustness.
**Next Steps:**
- **Testing:** Implement unit and integration tests to ensure all functionalities work as expected.
- **Frontend Integration:** Develop a frontend application to interact with this backend.
- **Deployment:** Deploy the application to a cloud provider like Heroku, AWS, or others.
- **Enhancements:** Consider adding features like file uploads for assignments, notifications, OAuth2 integration, etc.
Feel free to reach out if you need further assistance or clarifications!