Skip to content

# Solution #201

Description

@imwirelesscloud

Solution

Let's start by setting up a clear folder structure for our project:

/online-bookstore
|-- .env
|-- app.js
|-- /models
|   |-- User.js
|   |-- Book.js
|-- /routes
|   |-- auth.js
|   |-- books.js
|-- /middleware
|   |-- authorization.js
|   |-- logging.js
|-- /node_modules
|-- package.json

Step 1: Project Setup

In your terminal:

mkdir online-bookstore
cd online-bookstore
npm init -y
npm install express mongoose jsonwebtoken bcryptjs dotenv morgan validator helmet express-rate-limit winston

Create a .env file in the root of your project with:

MONGODB_URI="your_mongodb_uri"
JWT_SECRET="your_secret_key"

Step 2: Establish MongoDB Connection

In app.js:

const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');

dotenv.config();

mongoose.connect(process.env.MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
});

const app = express();

Step 3: Basic Express Setup

Continue in app.js:

app.use(express.json());

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

Step 4: Model Definitions

In /models/User.js:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
    username: String,
    password: String,
    role: String,
});

userSchema.pre('save', function(next) {
    this.password = bcrypt.hashSync(this.password, 12);
    next();
});

module.exports = mongoose.model('User', userSchema);

Step 5: Authentication

In /routes/auth.js:

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('../models/User');

const router = express.Router();

router.post('/login', async (req, res) => {
    const user = await User.findOne({ username: req.body.username });
    if (!user) return res.status(400).send('Invalid username or password.');

    const validPassword = bcrypt.compareSync(req.body.password, user.password);
    if (!validPassword) return res.status(400).send('Invalid username or password.');

    const token = jwt.sign({ _id: user._id, role: user.role }, process.env.JWT_SECRET);
    res.send(token);
});

module.exports = router;

Step 6: Authorization Middleware

In /middleware/authorization.js:

const jwt = require('jsonwebtoken');

function authorize(role) {
    return (req, res, next) => {
        const token = req.header('x-auth-token');
        if (!token) return res.status(401).send('No token provided.');

        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        if (!decoded || (role && decoded.role !== role)) return res.status(403).send('Forbidden.');

        req.user = decoded;
        next();
    }
}

module.exports = authorize;

Step 7: CRUD for Books

In /models/Book.js:

const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
    title: String,
    author: String,
    price: Number,
});

module.exports = mongoose.model('Book', bookSchema);

In /routes/books.js:

const express = require('express');
const Book = require('../models/Book');
const authorize = require('../middleware/authorization');

const router = express.Router();

// All your CRUD operations here, e.g.,
router.get('/', async (req, res) => {
    const books = await Book.find();
    res.send(books);
});

// Use the authorize middleware as needed, e.g., 
router.post('/', authorize('admin'), async (req, res) => {
    const book = new Book(req.body);
    await book.save();
    res.send(book);
});

module.exports = router;

Step 8: Logging Middleware

In app.js:

const morgan = require('morgan');
app.use(morgan('tiny'));

For error-handling, logging, input validation, security, rate limiting, user registration, and advanced logging, follow the same method to break down your code into modular sections and use middleware appropriately.

Remember to use the app.use('/desired-route', require('./routes/route-file')) to link your route files in the main app.js file.

Let's dive deeper and continue elaborating on the code.

Alternate Step 8: Logging Middleware

You've already added the morgan middleware for basic logging.

In app.js:

const morgan = require('morgan');
app.use(morgan('combined'));  // 'combined' gives detailed logs

Step 9: Error-handling

Add a centralized error-handling middleware at the bottom of your app.js:

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something went wrong!');
});

Step 10: Input Validation

In /routes/books.js, add some basic validation:

const { check, validationResult } = require('express-validator');

router.post('/', 
    [
        check('title').isLength({ min: 1 }).withMessage('Title is required'),
        check('author').isLength({ min: 1 }).withMessage('Author is required'),
        check('price').isNumeric().withMessage('Valid price is required'),
    ],
    async (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        
        // ... rest of the CRUD logic ...
    }
);

Part-2

Step 11: Securing the Application

You've already added helmet for basic security.

In app.js:

const helmet = require('helmet');
app.use(helmet());

Step 12: Rate Limiting

Again, in your main app.js:

const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limit each IP to 100 requests per windowMs
});

app.use(limiter);

Step 13: User Registration and Validation

In /routes/auth.js, append:

router.post('/register',
    [
        check('username', 'Username is required').not().isEmpty(),
        check('password', 'Password should be at least 6 chars long').isLength({ min: 6 }),
    ],
    async (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ errors: errors.array() });
        }
        
        const userExists = await User.findOne({ username: req.body.username });
        if (userExists) return res.status(400).send('Username already exists.');

        const user = new User(req.body);
        await user.save();
        res.send('User registered successfully!');
    }
);

Step 14: Testing with Postman

At this point, you would open Postman and test the following endpoints:

  1. POST to /register with { "username": "your_username", "password": "your_password" }
  2. POST to /login with { "username": "your_username", "password": "your_password" } and get a JWT token.
  3. Test CRUD operations on /books using the received JWT token.

Step 15: Logging and Monitoring

For advanced logging, use winston.

In app.js:

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    defaultMeta: { service: 'online-bookstore-service' },
    transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' }),
    ],
});

// To log directly to the console in a development setting:
if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.simple(),
    }));
}

// Replace console.error with logger.error and likewise for other log levels

Now, when you want to log anything, you can use logger.info('Your message'), logger.error('Your error message'), etc. These logs will be saved to the specified files.

This provides a comprehensive backend setup. Remember, in a real-world application, you would also want to set up proper HTTPS, ensure your environment variables are secure, and perhaps consider using a service like MongoDB Atlas for a more secure and scalable MongoDB solution.

Originally posted by @akash-coded in #192 (comment)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions