________________________________________ Disclaimer: This full-stack solution serves as a comprehensive guide in stages. Actual development may require adjustments based on specific requirements, resource availability, and evolving project dynamics. ________________________________________ 1. API-First Development 1.1. Design and document API endpoints 1.2. Implement API versioning 1.3. Develop API testing procedures ________________________________________ 2. Microservices Architecture 2.1. Design microservices based on feature modules 2.2. Implement API Gateway 2.3. Manage service discovery 2.4. Ensure scalability and fault tolerance ________________________________________ 3. Centralized Authentication 3.1. Select authentication framework 3.2. Implement unified authentication system 3.3. Integrate authentication across microservices 3.4. Implement Single Sign-On (SSO) ________________________________________ 4. Event-Driven Communication 4.1. Choose message broker 4.2. Define event schema and topics 4.3. Implement event publishing and subscription 4.4. Ensure reliable event processing ________________________________________ 5. Shared Libraries & SDKs 5.1. Develop shared frontend libraries 5.2. Develop shared backend libraries 5.3. Develop SDKs for third-party integrations 5.4. Maintain version control for libraries ________________________________________ 6. Consistent Data Models 6.1. Define centralized data schemas 6.2. Implement data normalization 6.3. Synchronize data across microservices 6.4. Implement data validation ________________________________________ 7. Comprehensive Testing 7.1. Develop unit tests 7.2. Implement integration tests 7.3. Conduct end-to-end (E2E) testing 7.4. Automate testing in CI/CD pipeline 7.5. Perform performance and load testing 7.6. Conduct security testing ________________________________________ 8. Version Control & Documentation 8.1. Establish version control practices 8.2. Maintain thorough documentation 8.3. Implement code reviews and quality standards 8.4. Use automated documentation tools 8.5. Maintain change logs ________________________________________ 9. Modular Frontend Development 9.1. Develop UI components for each feature 9.2. Implement state management with Redux 9.3. Build and integrate front-end features (e.g., ARTie, Custom Product Designer) 9.4. Ensure mobile responsiveness and accessibility ________________________________________ 10. Backend Development 10.1. Implement backend services for each feature (e.g., Product Management, Payment, Order Tracking) 10.2. Develop API endpoints and integrate them with the frontend 10.3. Handle data processing, including product imports and dynamic pricing 10.4. Implement background jobs for tasks like email notifications, image generation, etc. ________________________________________ 11. Payment & Payouts Integration 11.1. Integrate PayPal for payment processing 11.2. Implement payout API for commissions and vendor payments 11.3. Develop alternative payment gateway options 11.4. Set up transaction logging and security protocols ________________________________________ 12. Shipping & Order Management Integration 12.1. Integrate ShipStation for live shipping rates 12.2. Develop order tracking and management features 12.3. Implement reverse auction system for production jobs 12.4. Automate order fulfillment workflows ________________________________________ 13. User Dashboard & Features 13.1. Develop dashboard UI for product management, pricing, and SEO 13.2. Implement affiliate and commission management tools 13.3. Provide tools for site customization (layout, content, theme) 13.4. Integrate analytics and reporting features for users ________________________________________ 14. Social Media & Marketing Integration 14.1. Integrate social media platforms for brand listening and engagement 14.2. Implement social media post scheduling and automation 14.3. Develop reward system for affiliates based on social engagement 14.4. Enable social media sharing features for users ________________________________________ 15. Custom AI Integration (e.g., GPT, DALL·E 3) 15.1. Enable users to integrate and build custom GPT models 15.2. Implement AI-based design assistance with DALL·E 3 integration 15.3. Provide AI credits management and token purchasing 15.4. Ensure AI model security and compliance ________________________________________ 16. Product & Template Customization 16.1. Develop template system for various business types (schools, sports teams, fundraising) 16.2. Allow users to select, customize, and preview templates 16.3. Provide customization options for product layouts and themes 16.4. Enable template management and updates by admins ________________________________________ 17. Affiliate & Distributor Management 17.1. Set up distributor-specific templates and commission structures 17.2. Implement affiliate tracking and commission management 17.3. Develop fundraising store creation tools 17.4. Provide user performance tracking and reporting tools ________________________________________ 18. Domain & Subsite Integration 18.1. Integrate ENOM API for domain registration 18.2. Set up subsite creation and management 18.3. Allow custom domain mapping and DNS management 18.4. Develop "SHOP" button integration for existing sites ________________________________________ 19. Fundraising & Quick Store Setup 19.1. Develop quick store builder for fundraising campaigns 19.2. Implement live sales progress indicators 19.3. Provide customizable templates for fundraising campaigns 19.4. Enable limited-time campaign management ________________________________________ 20. BaaS Integration for Debit Card and FinTech Partnership 20.1. Integrate FinTech APIs for debit card issuance and management 20.2. Enable instant funding features for sales revenue 20.3. Provide user interfaces for managing bank accounts and transfers 20.4. Ensure regulatory compliance and security for financial transactions ________________________________________ This index list breaks down the development process into manageable stages, allowing for modular, incremental progress and ease of tracking. Each stage focuses on one area of the platform, ensuring features are developed and integrated smoothly before moving to the next stage. 4o mini You said: Some of the 20 expected features are not directly mentioned in your development stage schedule. Tell me in what stage each of the 20 expected features is developed. Those 20 expected features are: AI Assistance: ARTie (AI Resource for Tshirts and inspiring ecommerce) Product and Pricing Features Product Page Layout & Customization Decoration Methods & Pricing Markup & Discount Rules Order & Production Management Payment & Payouts using PayPal: Alternative Method and Payout APIs Domain and Subsite Integration User Dashboard and Features Social Media Integration & MarketingCustom AI Integration Affiliate & Commission Management, Credit Card for Chargebacks Shipping Integration Quick Quote popup Custom Product Designer Templates Expansion Fundraising Quick Store Builder Distributor-Specific Signup Builder BaaS Integration for Debit Card and FinTech Partnership 2/2 ChatGPT said: ChatGPT Certainly! Below is a mapping of each of the 20 expected features to the appropriate development stage in the modular, staged development process: ________________________________________ 1. AI Assistance: ARTie (AI Resource for T-shirts and Inspiring eCommerce) • Development Stage: o Stage 5: Shared Libraries & SDKs (for integration of AI models and tools) o Stage 14: Social Media & Marketing Integration (for social media interaction with ARTie) o Stage 16: Product & Template Customization (for product and template customization via ARTie) o Stage 3: Centralized Authentication (for integrating ARTie with user profiles) o Stage 7: Comprehensive Testing (for AI testing and interaction quality assurance) ________________________________________ 2. Product and Pricing Features • Development Stage: o Stage 5: Shared Libraries & SDKs (for API integration and pricing algorithms) o Stage 10: Backend Development (for product management, pricing, and database management) o Stage 6: Consistent Data Models (for pricing schema and normalization) o Stage 7: Comprehensive Testing (for pricing accuracy testing) ________________________________________ 3. Product Page Layout & Customization • Development Stage: o Stage 9: Modular Frontend Development (for product page customization UI) o Stage 16: Product & Template Customization (for product page layouts and templates) o Stage 5: Shared Libraries & SDKs (for frontend UI components) o Stage 10: Backend Development (for product data handling) o Stage 7: Comprehensive Testing (for UI and usability testing) ________________________________________ 4. Decoration Methods & Pricing • Development Stage: o Stage 10: Backend Development (for implementing decoration pricing models) o Stage 6: Consistent Data Models (for decoration pricing data structure) o Stage 5: Shared Libraries & SDKs (for reusable pricing algorithms) o Stage 7: Comprehensive Testing (for decoration pricing validation) ________________________________________ 5. Markup & Discount Rules • Development Stage: o Stage 10: Backend Development (for markup/discount rule implementation) o Stage 6: Consistent Data Models (for managing discount and markup structures) o Stage 5: Shared Libraries & SDKs (for creating reusable rule engines) o Stage 7: Comprehensive Testing (for rule engine testing) ________________________________________ 6. Order & Production Management • Development Stage: o Stage 10: Backend Development (for order management and integration with suppliers) o Stage 5: Shared Libraries & SDKs (for production management tools and workflows) o Stage 6: Consistent Data Models (for tracking order and production data) o Stage 4: Event-Driven Communication (for event-driven updates on order status) o Stage 7: Comprehensive Testing (for order processing and production flow testing) ________________________________________ 7. Payment & Payouts using PayPal: Alternative Method and Payout APIs • Development Stage: o Stage 10: Backend Development (for PayPal API integration and payout systems) o Stage 3: Centralized Authentication (for payment security and user authentication) o Stage 7: Comprehensive Testing (for payment system testing) ________________________________________ 8. Domain and Subsite Integration • Development Stage: o Stage 10: Backend Development (for domain registration and subsite creation) o Stage 5: Shared Libraries & SDKs (for domain management tools and APIs) o Stage 9: Modular Frontend Development (for subsite frontend integration) o Stage 7: Comprehensive Testing (for domain integration and subsite validation) ________________________________________ 9. User Dashboard and Features • Development Stage: o Stage 9: Modular Frontend Development (for user dashboard UI) o Stage 10: Backend Development (for user account management, pricing, and SEO tools) o Stage 7: Comprehensive Testing (for dashboard usability and functionality testing) ________________________________________ 10. Social Media Integration & Marketing • Development Stage: o Stage 14: Social Media & Marketing Integration (for integrating with social media platforms) o Stage 10: Backend Development (for social media management and marketing features) o Stage 9: Modular Frontend Development (for social media sharing and interaction UI) o Stage 7: Comprehensive Testing (for social media functionality testing) ________________________________________ 11. Custom AI Integration • Development Stage: o Stage 14: Social Media & Marketing Integration (for GPT-powered marketing tools) o Stage 5: Shared Libraries & SDKs (for creating custom AI models) o Stage 10: Backend Development (for AI integration with user stores) o Stage 7: Comprehensive Testing (for AI model testing and validation) ________________________________________ 12. Affiliate & Commission Management • Development Stage: o Stage 10: Backend Development (for commission and affiliate management logic) o Stage 9: Modular Frontend Development (for affiliate UI and tools) o Stage 7: Comprehensive Testing (for commission calculations and affiliate features) ________________________________________ 13. Credit Card for Chargebacks • Development Stage: o Stage 10: Backend Development (for integrating credit card payments and chargeback protection) o Stage 3: Centralized Authentication (for ensuring secure payment methods) o Stage 7: Comprehensive Testing (for credit card security and chargeback testing) ________________________________________ 14. Shipping Integration • Development Stage: o Stage 10: Backend Development (for integrating ShipStation and live shipping rates) o Stage 5: Shared Libraries & SDKs (for shipping API integration tools) o Stage 9: Modular Frontend Development (for displaying shipping options and rates) o Stage 7: Comprehensive Testing (for shipping rate calculations and functionality) ________________________________________ 15. Quick Quote Popup • Development Stage: o Stage 9: Modular Frontend Development (for designing and implementing the Quick Quote popup) o Stage 10: Backend Development (for integrating pricing calculations and API calls) o Stage 7: Comprehensive Testing (for Quick Quote functionality and accuracy testing) ________________________________________ 16. Custom Product Designer • Development Stage: o Stage 9: Modular Frontend Development (for creating the interactive design tool UI) o Stage 10: Backend Development (for handling product designs and data) o Stage 7: Comprehensive Testing (for designer functionality and performance) ________________________________________ 17. Templates Expansion • Development Stage: o Stage 10: Backend Development (for creating and managing templates and categories) o Stage 9: Modular Frontend Development (for displaying and customizing templates) o Stage 5: Shared Libraries & SDKs (for reusable template components) o Stage 7: Comprehensive Testing (for template customization and UI testing) ________________________________________ 18. Fundraising Quick Store Builder • Development Stage: o Stage 9: Modular Frontend Development (for fundraising store setup UI) o Stage 10: Backend Development (for creating fundraising campaigns and templates) o Stage 7: Comprehensive Testing (for fundraising workflow and functionality) ________________________________________ 19. Distributor-Specific Signup Builder • Development Stage: o Stage 10: Backend Development (for distributor signup and template management) o Stage 9: Modular Frontend Development (for distributor onboarding UI) o Stage 7: Comprehensive Testing (for distributor-specific signup testing) ________________________________________ 20. BaaS Integration for Debit Card and FinTech Partnership • Development Stage: o Stage 10: Backend Development (for integrating FinTech APIs and BaaS features) o Stage 5: Shared Libraries & SDKs (for developing reusable APIs for financial integrations) o Stage 9: Modular Frontend Development (for displaying financial tools and debit card management) o Stage 7: Comprehensive Testing (for ensuring regulatory compliance and secure financial transactions) ________________________________________ Summary of Development Stages: • Stage 5: Shared Libraries & SDKs: Used for AI, third-party integration, shipping, templates, and reusable components across features. • Stage 10: Backend Development: Core for the majority of the features, especially payment systems, product management, affiliate management, and backend logic. • Stage 9: Modular Frontend Development: Critical for UI-heavy features like custom product designers, templates, dashboards, and social media integration. • Stage 7: Comprehensive Testing: Ensures that all components, especially complex features like pricing, payments, and AI models, are properly validated and secure. • Stage 3: Centralized Authentication: Ensures secure user access for features like payment, commissions, AI, and order management. • Stage 14: Social Media & Marketing Integration: Essential for integrating social media functionality, GPT-powered tools, and marketing systems. • Stage 4: Event-Driven Communication: Used in features like order management, event notifications, and shipping integrations. This mapping aligns each feature to the correct development stage, ensuring that all functionalities are developed and tested efficiently in a modular fashion.
Let's embark on building Stage 1: API-First Development for your eCommerce platform, iBranDIDit.com. This stage lays the foundation for all subsequent features by establishing a well-documented and versioned API. We'll use Node.js with Express.js for the backend, Swagger for API documentation, and Jest with Supertest for testing. Stage 1: API-First Development ________________________________________ 1. Project Setup 1.1. Prerequisites Ensure you have the following installed on your machine: • Node.js (v14.x or later) • npm (Node Package Manager) • Git 1.2. Initialize the Project 1. Create Project Directory bash Copy code mkdir ibrandidit-api cd ibrandidit-api 2. Initialize npm bash Copy code npm init -y This creates a package.json file with default settings. 1.3. Install Dependencies We'll install essential packages for API development, documentation, and testing. bash Copy code npm install express cors body-parser dotenv npm install --save-dev nodemon jest supertest swagger-jsdoc swagger-ui-express • express: Web framework for Node.js • cors: Middleware for enabling CORS • body-parser: Middleware to parse incoming request bodies • dotenv: Loads environment variables from a .env file • nodemon: Utility that monitors for changes and automatically restarts the server • jest: JavaScript testing framework • supertest: HTTP assertions for testing • swagger-jsdoc & swagger-ui-express: Tools for API documentation 1.4. Configure package.json Scripts Update the package.json to include scripts for development, testing, and starting the server. json Copy code { "name": "ibrandidit-api", "version": "1.0.0", "description": "API-First Development for iBranDIDit.com", "main": "index.js", "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest", "swagger-autogen": "node swagger.js" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^4.6.3" }, "devDependencies": { "jest": "^29.6.1", "nodemon": "^3.0.1", "supertest": "^6.3.3" } } 1.5. Create Project Structure Organize your project with a clear structure. arduino Copy code ibrandidit-api/ ├── controllers/ │ └── userController.js ├── routes/ │ └── userRoutes.js ├── tests/ │ └── user.test.js ├── .env ├── index.js ├── jest.config.js ├── swagger.js └── README.md 1.6. Create Essential Files 1. Create .env File This file will store environment variables. env Copy code PORT=5000 NODE_ENV=development 2. Create index.js This is the entry point of your application. javascript Copy code // index.js const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const dotenv = require('dotenv'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./swagger'); // Load environment variables dotenv.config(); const app = express(); // Middleware app.use(cors()); app.use(bodyParser.json()); // Routes const userRoutes = require('./routes/userRoutes'); app.use('/api/v1/users', userRoutes); // Swagger Documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Root Route app.get('/', (req, res) => { res.send('Welcome to iBranDIDit.com API'); }); // Start Server const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); module.exports = app; // Export app for testing 3. Create swagger.js This file sets up Swagger for API documentation. javascript Copy code // swagger.js const swaggerJSDoc = require('swagger-jsdoc'); const options = { definition: { openapi: '3.0.0', info: { title: 'iBranDIDit.com API', version: '1.0.0', description: 'API documentation for iBranDIDit.com eCommerce platform', }, servers: [ { url: 'http://localhost:5000', description: 'Development server', }, ], }, apis: ['./routes/*.js', './controllers/*.js'], }; const swaggerSpec = swaggerJSDoc(options); module.exports = swaggerSpec; 4. Create jest.config.js Configure Jest for testing. javascript Copy code // jest.config.js module.exports = { testEnvironment: 'node', }; 5. Create README.md Provide an overview and setup instructions. markdown Copy code # iBranDIDit.com API API-First Development for iBranDIDit.com eCommerce platform. ## Table of Contents - [Installation](#installation) - [Running the Server](#running-the-server) - [API Documentation](#api-documentation) - [Testing](#testing) - [Project Structure](#project-structure) ## Installation 1. **Clone the repository** ```bash git clone https://github.com/yourusername/ibrandidit-api.git cd ibrandidit-api 2. Install dependencies bash Copy code npm install 3. Configure Environment Variables Create a .env file in the root directory: env Copy code PORT=5000 NODE_ENV=development Running the Server o Development Mode bash Copy code npm run dev Uses nodemon for automatic restarts on file changes. o Production Mode bash Copy code npm start API Documentation Access the Swagger UI for API documentation at: http://localhost:5000/api-docs Testing Run tests using Jest: bash Copy code npm test Project Structure arduino Copy code ibrandidit-api/ ├── controllers/ │ └── userController.js ├── routes/ │ └── userRoutes.js ├── tests/ │ └── user.test.js ├── .env ├── index.js ├── jest.config.js ├── swagger.js └── README.md Copy code ________________________________________ 2. Implementing API Endpoints We'll create a simple User API as an example. You can replicate similar structures for other entities (e.g., Products, Orders). 2.1. Create User Controller javascript Copy code // controllers/userController.js /** * @swagger * tags: * name: Users * description: User management and login */ /** * @swagger * /api/v1/users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' */ exports.getUsers = (req, res) => { // Dummy data for demonstration const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; res.status(200).json(users); }; /** * @swagger * /api/v1/users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ exports.getUserById = (req, res) => { const userId = parseInt(req.params.id); // Dummy data for demonstration const user = { id: userId, name: 'Alice', email: 'alice@example.com' }; if (user.id === userId) { res.status(200).json(user); } else { res.status(404).json({ message: 'User not found' }); } }; /** * @swagger * /api/v1/users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 400: * description: Invalid input */ exports.createUser = (req, res) => { const { name, email } = req.body; if (name && email) { const newUser = { id: Date.now(), name, email }; // In a real application, you'd save the user to the database here res.status(201).json(newUser); } else { res.status(400).json({ message: 'Name and email are required' }); } }; 2.2. Create User Routes javascript Copy code // routes/userRoutes.js const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); /** * @swagger * components: * schemas: * User: * type: object * required: * - id * - name * - email * properties: * id: * type: integer * description: The auto-generated id of the user * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * id: 1 * name: Alice * email: alice@example.com * UserInput: * type: object * required: * - name * - email * properties: * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * name: Alice * email: alice@example.com */ /** * @swagger * /api/v1/users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. */ router.get('/', userController.getUsers); /** * @swagger * /api/v1/users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - name: id * in: path * required: true * description: ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * 404: * description: User not found */ router.get('/:id', userController.getUserById); /** * @swagger * /api/v1/users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * 400: * description: Invalid input */ router.post('/', userController.createUser); module.exports = router; 2.3. Testing the API We'll use Jest and Supertest to write tests for our User API. 1. Create Test File bash Copy code mkdir tests touch tests/user.test.js 2. Write Tests in tests/user.test.js javascript Copy code // tests/user.test.js const request = require('supertest'); const app = require('../index'); describe('User API Endpoints', () => { it('should retrieve a list of users', async () => { const res = await request(app).get('/api/v1/users'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBeTruthy(); }); it('should retrieve a user by ID', async () => { const res = await request(app).get('/api/v1/users/1'); expect(res.statusCode).toEqual(200); expect(res.body).toHaveProperty('id', 1); }); it('should return 404 for a non-existent user', async () => { const res = await request(app).get('/api/v1/users/999'); expect(res.statusCode).toEqual(404); expect(res.body).toHaveProperty('message', 'User not found'); }); it('should create a new user', async () => { const res = await request(app) .post('/api/v1/users') .send({ name: 'Charlie', email: 'charlie@example.com', }); expect(res.statusCode).toEqual(201); expect(res.body).toHaveProperty('name', 'Charlie'); expect(res.body).toHaveProperty('email', 'charlie@example.com'); }); it('should return 400 for invalid user input', async () => { const res = await request(app) .post('/api/v1/users') .send({ name: '', email: '', }); expect(res.statusCode).toEqual(400); expect(res.body).toHaveProperty('message', 'Name and email are required'); }); }); 3. Run Tests Add the following configuration to package.json to specify Jest settings. json Copy code "jest": { "testEnvironment": "node" } Run the tests: bash Copy code npm test You should see output indicating the tests have passed. 2.4. Implement API Versioning Versioning ensures that future changes to the API don't break existing clients. We'll implement URI versioning by prefixing routes with /api/v1. Example: • Current Version: /api/v1/users • Future Version: /api/v2/users This approach is already reflected in our route definitions. 2.5. Run the Server Start the server in development mode with automatic restarts: bash Copy code npm run dev You should see: arduino Copy code Server running in development mode on port 5000 2.6. Accessing API Documentation Open your browser and navigate to: http://localhost:5000/api-docs You'll see the Swagger UI with documentation for your User API. ________________________________________ 3. Complete Code and Configuration Files 3.1. package.json json Copy code { "name": "ibrandidit-api", "version": "1.0.0", "description": "API-First Development for iBranDIDit.com", "main": "index.js", "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest", "swagger-autogen": "node swagger.js" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^4.6.3" }, "devDependencies": { "jest": "^29.6.1", "nodemon": "^3.0.1", "supertest": "^6.3.3" }, "jest": { "testEnvironment": "node" } } 3.2. .env env Copy code PORT=5000 NODE_ENV=development 3.3. index.js javascript Copy code // index.js const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const dotenv = require('dotenv'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./swagger'); // Load environment variables dotenv.config(); const app = express(); // Middleware app.use(cors()); app.use(bodyParser.json()); // Routes const userRoutes = require('./routes/userRoutes'); app.use('/api/v1/users', userRoutes); // Swagger Documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Root Route app.get('/', (req, res) => { res.send('Welcome to iBranDIDit.com API'); }); // Start Server const PORT = process.env.PORT || 5000; app.listen(PORT, () => { console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); module.exports = app; // Export app for testing 3.4. swagger.js javascript Copy code // swagger.js const swaggerJSDoc = require('swagger-jsdoc'); const options = { definition: { openapi: '3.0.0', info: { title: 'iBranDIDit.com API', version: '1.0.0', description: 'API documentation for iBranDIDit.com eCommerce platform', }, servers: [ { url: 'http://localhost:5000', description: 'Development server', }, ], }, apis: ['./routes/*.js', './controllers/*.js'], }; const swaggerSpec = swaggerJSDoc(options); module.exports = swaggerSpec; 3.5. jest.config.js javascript Copy code // jest.config.js module.exports = { testEnvironment: 'node', }; 3.6. controllers/userController.js javascript Copy code // controllers/userController.js /** * @swagger * tags: * name: Users * description: User management and login */ /** * @swagger * /api/v1/users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' */ exports.getUsers = (req, res) => { // Dummy data for demonstration const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; res.status(200).json(users); }; /** * @swagger * /api/v1/users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ exports.getUserById = (req, res) => { const userId = parseInt(req.params.id); // Dummy data for demonstration const user = { id: userId, name: 'Alice', email: 'alice@example.com' }; if (user.id === userId) { res.status(200).json(user); } else { res.status(404).json({ message: 'User not found' }); } }; /** * @swagger * /api/v1/users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 400: * description: Invalid input */ exports.createUser = (req, res) => { const { name, email } = req.body; if (name && email) { const newUser = { id: Date.now(), name, email }; // In a real application, you'd save the user to the database here res.status(201).json(newUser); } else { res.status(400).json({ message: 'Name and email are required' }); } }; 3.7. routes/userRoutes.js javascript Copy code // routes/userRoutes.js const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); /** * @swagger * components: * schemas: * User: * type: object * required: * - id * - name * - email * properties: * id: * type: integer * description: The auto-generated id of the user * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * id: 1 * name: Alice * email: alice@example.com * UserInput: * type: object * required: * - name * - email * properties: * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * name: Alice * email: alice@example.com */ /** * @swagger * /api/v1/users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. */ router.get('/', userController.getUsers); /** * @swagger * /api/v1/users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - name: id * in: path * required: true * description: ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * 404: * description: User not found */ router.get('/:id', userController.getUserById); /** * @swagger * /api/v1/users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * 400: * description: Invalid input */ router.post('/', userController.createUser); module.exports = router; 3.8. tests/user.test.js javascript Copy code // tests/user.test.js const request = require('supertest'); const app = require('../index'); describe('User API Endpoints', () => { it('should retrieve a list of users', async () => { const res = await request(app).get('/api/v1/users'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBeTruthy(); }); it('should retrieve a user by ID', async () => { const res = await request(app).get('/api/v1/users/1'); expect(res.statusCode).toEqual(200); expect(res.body).toHaveProperty('id', 1); }); it('should return 404 for a non-existent user', async () => { const res = await request(app).get('/api/v1/users/999'); expect(res.statusCode).toEqual(404); expect(res.body).toHaveProperty('message', 'User not found'); }); it('should create a new user', async () => { const res = await request(app) .post('/api/v1/users') .send({ name: 'Charlie', email: 'charlie@example.com', }); expect(res.statusCode).toEqual(201); expect(res.body).toHaveProperty('name', 'Charlie'); expect(res.body).toHaveProperty('email', 'charlie@example.com'); }); it('should return 400 for invalid user input', async () => { const res = await request(app) .post('/api/v1/users') .send({ name: '', email: '', }); expect(res.statusCode).toEqual(400); expect(res.body).toHaveProperty('message', 'Name and email are required'); }); }); ________________________________________ 4. Running the Project 4.1. Start the Server bash Copy code npm run dev You should see: arduino Copy code Server running in development mode on port 5000 4.2. Accessing the API • List Users http Copy code GET http://localhost:5000/api/v1/users • Get User by ID http Copy code GET http://localhost:5000/api/v1/users/1 • Create a New User http Copy code POST http://localhost:5000/api/v1/users Content-Type: application/json { "name": "Charlie", "email": "charlie@example.com" } 4.3. Viewing API Documentation Navigate to http://localhost:5000/api-docs in your browser to view the Swagger UI with detailed API documentation. ________________________________________ 5. Summary and Next Steps 5.1. Summary • Project Initialization: Set up a Node.js project with Express.js, configured essential dependencies, and structured the project directories. • API Implementation: Created basic User API endpoints (GET, POST) with Swagger documentation. • API Versioning: Implemented URI versioning (/api/v1/). • Testing: Set up Jest and Supertest for automated testing of API endpoints. • Documentation: Configured Swagger for interactive API documentation accessible via /api-docs. 5.2. Next Steps With Stage 1 completed, you're now ready to proceed to Stage 2: Microservices Architecture. This will involve breaking down the application into smaller, manageable services, each responsible for specific functionalities. Here's a brief on what to expect: 1. Stage 2: Microservices Architecture o Design and implement microservices based on feature modules. o Set up an API Gateway for routing and load balancing. o Implement service discovery for dynamic microservice management. o Ensure scalability and fault tolerance through auto-scaling and circuit breakers. Each subsequent stage will build upon the foundation laid in Stage 1, ensuring a robust and scalable architecture for your eCommerce platform. ________________________________________ 6. Additional Resources • Express.js Documentation: https://expressjs.com/ • Swagger Documentation: https://swagger.io/docs/ • Jest Documentation: https://jestjs.io/docs/getting-started • Supertest Documentation: https://github.com/visionmedia/supertest ________________________________________ 7. Troubleshooting Tips • Server Not Starting: o Ensure all dependencies are installed correctly (npm install). o Check if the port specified in .env is not in use by another application. • Tests Failing: o Ensure the server is running (npm run dev) before running tests. o Verify the test data and endpoints match those defined in your routes and controllers. • Swagger UI Not Loading: o Confirm that swagger.js is correctly exporting the Swagger specification. o Ensure that the apis path in swagger.js correctly points to your route and controller files. ________________________________________ By following this guide, you've successfully set up the foundational API structure for your eCommerce platform. As you progress through the subsequent stages, continue to build upon this structure, ensuring consistency and modularity throughout your development process. ________________________________________ Certainly! Let's proceed to Stage 2: Microservices Architecture for your eCommerce platform, iBranDIDit.com. This stage involves breaking down your monolithic API into smaller, manageable microservices, setting up an API Gateway to route requests, implementing service discovery, and ensuring scalability and fault tolerance. We'll use Node.js with Express.js for the microservices, Express Gateway as the API Gateway, and Docker Compose for orchestration. Stage 2: Microservices Architecture ________________________________________ 1. Overview Objective: Transform the monolithic API developed in Stage 1 into a microservices architecture by: 1. Creating Separate Microservices for distinct functionalities. 2. Implementing an API Gateway to manage and route requests. 3. Setting Up Service Discovery to allow services to locate each other. 4. Ensuring Scalability and Fault Tolerance through Docker Compose configurations. Benefits: • Modularity: Each service handles a specific business capability. • Scalability: Services can be scaled independently based on demand. • Maintainability: Easier to manage, update, and deploy individual services. • Fault Isolation: Issues in one service don't directly impact others. ________________________________________ 2. Technology Stack • Backend Framework: Node.js with Express.js • API Gateway: Express Gateway • Containerization: Docker • Orchestration: Docker Compose • Service Discovery: Docker Compose networking • Documentation: Swagger (integrated into microservices) • Testing: Jest and Supertest ________________________________________ 3. Project Structure We'll organize the project into separate directories for each microservice and the API Gateway. Here's the proposed structure: lua Copy code ibrandidit-microservices/ ├── api-gateway/ │ ├── config/ │ │ └── gateway.config.yml │ ├── package.json │ └── README.md ├── user-service/ │ ├── controllers/ │ │ └── userController.js │ ├── routes/ │ │ └── userRoutes.js │ ├── tests/ │ │ └── user.test.js │ ├── .env │ ├── index.js │ ├── jest.config.js │ ├── swagger.js │ ├── package.json │ └── README.md ├── product-service/ │ ├── controllers/ │ │ └── productController.js │ ├── routes/ │ │ └── productRoutes.js │ ├── tests/ │ │ └── product.test.js │ ├── .env │ ├── index.js │ ├── jest.config.js │ ├── swagger.js │ ├── package.json │ └── README.md ├── docker-compose.yml └── README.md ________________________________________ 4. Implementation Steps 4.1. Setting Up Microservices We'll create two microservices for demonstration: 1. User Service: Handles user-related operations (from Stage 1). 2. Product Service: Manages product-related functionalities. 4.1.1. User Service a. Create Directory and Initialize Project bash Copy code mkdir -p ibrandidit-microservices/user-service cd ibrandidit-microservices/user-service npm init -y b. Install Dependencies bash Copy code npm install express cors body-parser dotenv npm install --save-dev nodemon jest supertest swagger-jsdoc swagger-ui-express c. Create package.json Scripts Update the package.json to include scripts for development, testing, and starting the server. json Copy code { "name": "user-service", "version": "1.0.0", "description": "User Service for iBranDIDit.com", "main": "index.js", "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest", "swagger-autogen": "node swagger.js" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^4.6.3" }, "devDependencies": { "jest": "^29.6.1", "nodemon": "^3.0.1", "supertest": "^6.3.3" }, "jest": { "testEnvironment": "node" } } d. Create Project Files • Create .env env Copy code PORT=5001 NODE_ENV=development • Create index.js javascript Copy code // index.js const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const dotenv = require('dotenv'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./swagger'); // Load environment variables dotenv.config(); const app = express(); // Middleware app.use(cors()); app.use(bodyParser.json()); // Routes const userRoutes = require('./routes/userRoutes'); app.use('/users', userRoutes); // Swagger Documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Root Route app.get('/', (req, res) => { res.send('Welcome to User Service API'); }); // Start Server const PORT = process.env.PORT || 5001; app.listen(PORT, () => { console.log(`User Service running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); module.exports = app; // Export app for testing • Create swagger.js javascript Copy code // swagger.js const swaggerJSDoc = require('swagger-jsdoc'); const options = { definition: { openapi: '3.0.0', info: { title: 'iBranDIDit.com User Service API', version: '1.0.0', description: 'API documentation for the User Service of iBranDIDit.com eCommerce platform', }, servers: [ { url: 'http://localhost:5001', description: 'Development server', }, ], }, apis: ['./routes/*.js', './controllers/*.js'], }; const swaggerSpec = swaggerJSDoc(options); module.exports = swaggerSpec; • Create routes/userRoutes.js javascript Copy code // routes/userRoutes.js const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); /** * @swagger * tags: * name: Users * description: User management and login */ /** * @swagger * components: * schemas: * User: * type: object * required: * - id * - name * - email * properties: * id: * type: integer * description: The auto-generated id of the user * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * id: 1 * name: Alice * email: alice@example.com * UserInput: * type: object * required: * - name * - email * properties: * name: * type: string * description: The name of the user * email: * type: string * description: The email of the user * example: * name: Alice * email: alice@example.com */ /** * @swagger * /users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' */ router.get('/', userController.getUsers); /** * @swagger * /users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ router.get('/:id', userController.getUserById); /** * @swagger * /users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 400: * description: Invalid input */ router.post('/', userController.createUser); module.exports = router; • Create controllers/userController.js javascript Copy code // controllers/userController.js /** * @swagger * tags: * name: Users * description: User management and login */ /** * @swagger * /users: * get: * summary: Retrieve a list of users * tags: [Users] * responses: * 200: * description: A list of users. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' */ exports.getUsers = (req, res) => { // Dummy data for demonstration const users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }, ]; res.status(200).json(users); }; /** * @swagger * /users/{id}: * get: * summary: Get a user by ID * tags: [Users] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the user to retrieve * schema: * type: integer * responses: * 200: * description: User found * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 404: * description: User not found */ exports.getUserById = (req, res) => { const userId = parseInt(req.params.id); // Dummy data for demonstration const user = { id: userId, name: 'Alice', email: 'alice@example.com' }; if (user.id === userId) { res.status(200).json(user); } else { res.status(404).json({ message: 'User not found' }); } }; /** * @swagger * /users: * post: * summary: Create a new user * tags: [Users] * requestBody: * description: User object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/UserInput' * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/User' * 400: * description: Invalid input */ exports.createUser = (req, res) => { const { name, email } = req.body; if (name && email) { const newUser = { id: Date.now(), name, email }; // In a real application, you'd save the user to the database here res.status(201).json(newUser); } else { res.status(400).json({ message: 'Name and email are required' }); } }; • Create tests/user.test.js javascript Copy code // tests/user.test.js const request = require('supertest'); const app = require('../index'); describe('User API Endpoints', () => { it('should retrieve a list of users', async () => { const res = await request(app).get('/users'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBeTruthy(); }); it('should retrieve a user by ID', async () => { const res = await request(app).get('/users/1'); expect(res.statusCode).toEqual(200); expect(res.body).toHaveProperty('id', 1); }); it('should return 404 for a non-existent user', async () => { const res = await request(app).get('/users/999'); expect(res.statusCode).toEqual(404); expect(res.body).toHaveProperty('message', 'User not found'); }); it('should create a new user', async () => { const res = await request(app) .post('/users') .send({ name: 'Charlie', email: 'charlie@example.com', }); expect(res.statusCode).toEqual(201); expect(res.body).toHaveProperty('name', 'Charlie'); expect(res.body).toHaveProperty('email', 'charlie@example.com'); }); it('should return 400 for invalid user input', async () => { const res = await request(app) .post('/users') .send({ name: '', email: '', }); expect(res.statusCode).toEqual(400); expect(res.body).toHaveProperty('message', 'Name and email are required'); }); }); e. Testing the User Service 1. Run Tests bash Copy code npm test You should see output indicating that all tests have passed. 2. Start the User Service bash Copy code npm run dev You should see: sql Copy code User Service running in development mode on port 5001 3. Access User Service API Documentation Open your browser and navigate to: http://localhost:5001/api-docs You'll see the Swagger UI with documentation for your User Service API. 4.1.2. Product Service a. Create Directory and Initialize Project bash Copy code cd ../ mkdir -p product-service cd product-service npm init -y b. Install Dependencies bash Copy code npm install express cors body-parser dotenv npm install --save-dev nodemon jest supertest swagger-jsdoc swagger-ui-express c. Create package.json Scripts Update the package.json to include scripts for development, testing, and starting the server. json Copy code { "name": "product-service", "version": "1.0.0", "description": "Product Service for iBranDIDit.com", "main": "index.js", "scripts": { "start": "node index.js", "dev": "nodemon index.js", "test": "jest", "swagger-autogen": "node swagger.js" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^4.6.3" }, "devDependencies": { "jest": "^29.6.1", "nodemon": "^3.0.1", "supertest": "^6.3.3" }, "jest": { "testEnvironment": "node" } } d. Create Project Files • Create .env env Copy code PORT=5002 NODE_ENV=development • Create index.js javascript Copy code // index.js const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const dotenv = require('dotenv'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./swagger'); // Load environment variables dotenv.config(); const app = express(); // Middleware app.use(cors()); app.use(bodyParser.json()); // Routes const productRoutes = require('./routes/productRoutes'); app.use('/products', productRoutes); // Swagger Documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // Root Route app.get('/', (req, res) => { res.send('Welcome to Product Service API'); }); // Start Server const PORT = process.env.PORT || 5002; app.listen(PORT, () => { console.log(`Product Service running in ${process.env.NODE_ENV} mode on port ${PORT}`); }); module.exports = app; // Export app for testing • Create swagger.js javascript Copy code // swagger.js const swaggerJSDoc = require('swagger-jsdoc'); const options = { definition: { openapi: '3.0.0', info: { title: 'iBranDIDit.com Product Service API', version: '1.0.0', description: 'API documentation for the Product Service of iBranDIDit.com eCommerce platform', }, servers: [ { url: 'http://localhost:5002', description: 'Development server', }, ], }, apis: ['./routes/*.js', './controllers/*.js'], }; const swaggerSpec = swaggerJSDoc(options); module.exports = swaggerSpec; • Create routes/productRoutes.js javascript Copy code // routes/productRoutes.js const express = require('express'); const router = express.Router(); const productController = require('../controllers/productController'); /** * @swagger * tags: * name: Products * description: Product management and retrieval */ /** * @swagger * components: * schemas: * Product: * type: object * required: * - id * - name * - price * properties: * id: * type: integer * description: The auto-generated id of the product * name: * type: string * description: The name of the product * price: * type: number * description: The price of the product * example: * id: 1 * name: T-Shirt * price: 19.99 * ProductInput: * type: object * required: * - name * - price * properties: * name: * type: string * description: The name of the product * price: * type: number * description: The price of the product * example: * name: Hoodie * price: 29.99 */ /** * @swagger * /products: * get: * summary: Retrieve a list of products * tags: [Products] * responses: * 200: * description: A list of products. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Product' */ router.get('/', productController.getProducts); /** * @swagger * /products/{id}: * get: * summary: Get a product by ID * tags: [Products] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the product to retrieve * schema: * type: integer * responses: * 200: * description: Product found * content: * application/json: * schema: * $ref: '#/components/schemas/Product' * 404: * description: Product not found */ router.get('/:id', productController.getProductById); /** * @swagger * /products: * post: * summary: Create a new product * tags: [Products] * requestBody: * description: Product object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/ProductInput' * responses: * 201: * description: Product created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/Product' * 400: * description: Invalid input */ router.post('/', productController.createProduct); module.exports = router; • Create controllers/productController.js javascript Copy code // controllers/productController.js /** * @swagger * tags: * name: Products * description: Product management and retrieval */ /** * @swagger * /products: * get: * summary: Retrieve a list of products * tags: [Products] * responses: * 200: * description: A list of products. * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/Product' */ exports.getProducts = (req, res) => { // Dummy data for demonstration const products = [ { id: 1, name: 'T-Shirt', price: 19.99 }, { id: 2, name: 'Hoodie', price: 29.99 }, ]; res.status(200).json(products); }; /** * @swagger * /products/{id}: * get: * summary: Get a product by ID * tags: [Products] * parameters: * - in: path * name: id * required: true * description: Numeric ID of the product to retrieve * schema: * type: integer * responses: * 200: * description: Product found * content: * application/json: * schema: * $ref: '#/components/schemas/Product' * 404: * description: Product not found */ exports.getProductById = (req, res) => { const productId = parseInt(req.params.id); // Dummy data for demonstration const product = { id: productId, name: 'T-Shirt', price: 19.99 }; if (product.id === productId) { res.status(200).json(product); } else { res.status(404).json({ message: 'Product not found' }); } }; /** * @swagger * /products: * post: * summary: Create a new product * tags: [Products] * requestBody: * description: Product object that needs to be added * required: true * content: * application/json: * schema: * $ref: '#/components/schemas/ProductInput' * responses: * 201: * description: Product created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/Product' * 400: * description: Invalid input */ exports.createProduct = (req, res) => { const { name, price } = req.body; if (name && price) { const newProduct = { id: Date.now(), name, price }; // In a real application, you'd save the product to the database here res.status(201).json(newProduct); } else { res.status(400).json({ message: 'Name and price are required' }); } }; • Create tests/product.test.js javascript Copy code // tests/product.test.js const request = require('supertest'); const app = require('../index'); describe('Product API Endpoints', () => { it('should retrieve a list of products', async () => { const res = await request(app).get('/products'); expect(res.statusCode).toEqual(200); expect(Array.isArray(res.body)).toBeTruthy(); }); it('should retrieve a product by ID', async () => { const res = await request(app).get('/products/1'); expect(res.statusCode).toEqual(200); expect(res.body).toHaveProperty('id', 1); }); it('should return 404 for a non-existent product', async () => { const res = await request(app).get('/products/999'); expect(res.statusCode).toEqual(404); expect(res.body).toHaveProperty('message', 'Product not found'); }); it('should create a new product', async () => { const res = await request(app) .post('/products') .send({ name: 'Cap', price: 9.99, }); expect(res.statusCode).toEqual(201); expect(res.body).toHaveProperty('name', 'Cap'); expect(res.body).toHaveProperty('price', 9.99); }); it('should return 400 for invalid product input', async () => { const res = await request(app) .post('/products') .send({ name: '', price: '', }); expect(res.statusCode).toEqual(400); expect(res.body).toHaveProperty('message', 'Name and price are required'); }); }); e. Testing the Product Service 1. Run Tests bash Copy code npm test You should see output indicating that all tests have passed. 2. Start the Product Service bash Copy code npm run dev You should see: arduino Copy code Product Service running in development mode on port 5002 3. Access Product Service API Documentation Open your browser and navigate to: http://localhost:5002/api-docs You'll see the Swagger UI with documentation for your Product Service API. 4.2. Implementing API Gateway An API Gateway serves as the single entry point for all client requests, routing them to the appropriate microservices. We'll use Express Gateway for this purpose. a. Create API Gateway Directory and Initialize Project bash Copy code cd ../ mkdir -p api-gateway cd api-gateway npm init -y b. Install Express Gateway Express Gateway simplifies API Gateway setup with built-in features like authentication, rate limiting, and more. bash Copy code npm install -g express-gateway Note: Installing Express Gateway globally allows you to use its CLI commands. c. Initialize Express Gateway bash Copy code eg gateway create You'll be prompted to provide some information: • Gateway Name: api-gateway • Configuration Folder: ./ (current directory) • Plugins: Choose default options or add necessary plugins as needed. d. Project Structure After initialization, your api-gateway directory should look like this: lua Copy code api-gateway/ ├── config/ │ ├── gateway.config.yml │ ├── policies/ │ ├── pipelines/ │ └── serviceEndpoints/ ├── node_modules/ ├── package.json └── README.md e. Configure Service Endpoints We'll define the internal endpoints for user-service and product-service. Edit config/gateway.config.yml yaml Copy code http: port: 8080 apiEndpoints: users: path: /users target: http://user-service:5001 methods: GET, POST userById: path: /users/:id target: http://user-service:5001 methods: GET products: path: /products target: http://product-service:5002 methods: GET, POST productById: path: /products/:id target: http://product-service:5002 methods: GET serviceEndpoints: user-service: url: 'http://user-service:5001' product-service: url: 'http://product-service:5002' policies: - proxy pipelines: default: apiEndpoints: - users - userById - products - productById policies: - proxy: - action: serviceEndpoint: '{{request.params.service}}-service' Explanation: • http.port: The port on which the API Gateway will listen (8080). • apiEndpoints: Defines the public endpoints and maps them to internal service endpoints. • serviceEndpoints: Specifies the internal services' URLs. • policies: Uses the proxy policy to route requests. • pipelines: Connects API endpoints to policies. f. Update config/gateway.config.yml Ensure that the API Gateway routes requests correctly to the respective services. yaml Copy code http: port: 8080 apiEndpoints: users: path: /users target: http://user-service:5001 methods: GET, POST userById: path: /users/:id target: http://user-service:5001 methods: GET products: path: /products target: http://product-service:5002 methods: GET, POST productById: path: /products/:id target: http://product-service:5002 methods: GET serviceEndpoints: user-service: url: 'http://user-service:5001' product-service: url: 'http://product-service:5002' policies: - proxy pipelines: usersPipeline: apiEndpoints: - users policies: - proxy: - action: serviceEndpoint: user-service userByIdPipeline: apiEndpoints: - userById policies: - proxy: - action: serviceEndpoint: user-service productsPipeline: apiEndpoints: - products policies: - proxy: - action: serviceEndpoint: product-service productByIdPipeline: apiEndpoints: - productById policies: - proxy: - action: serviceEndpoint: product-service g. Create package.json Scripts Update the package.json to include scripts for starting the API Gateway. json Copy code { "name": "api-gateway", "version": "1.0.0", "description": "API Gateway for iBranDIDit.com", "main": "index.js", "scripts": { "start": "express-gateway start" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "express-gateway": "^2.30.0" } } h. Start the API Gateway bash Copy code npm install npm run start You should see output indicating that the API Gateway is running on port 8080. i. Testing the API Gateway 1. List Users via API Gateway http Copy code GET http://localhost:8080/users Expected Response: json Copy code [ { "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" } ] 2. Get User by ID via API Gateway http Copy code GET http://localhost:8080/users/1 Expected Response: json Copy code { "id": 1, "name": "Alice", "email": "alice@example.com" } 3. List Products via API Gateway http Copy code GET http://localhost:8080/products Expected Response: json Copy code [ { "id": 1, "name": "T-Shirt", "price": 19.99 }, { "id": 2, "name": "Hoodie", "price": 29.99 } ] 4. Get Product by ID via API Gateway http Copy code GET http://localhost:8080/products/1 Expected Response: json Copy code { "id": 1, "name": "T-Shirt", "price": 19.99 } j. Add Swagger Documentation for API Gateway Express Gateway doesn't have built-in Swagger documentation. To provide centralized API documentation, you can aggregate Swagger docs from individual services or manually document routes. For simplicity, we'll skip this step for the API Gateway and rely on individual service documentation. ________________________________________ 4. Service Discovery In a Docker Compose environment, service discovery is simplified through Docker's internal DNS. Services can communicate with each other using their service names as hostnames. For example: • User Service URL: http://user-service:5001 • Product Service URL: http://product-service:5002 The API Gateway uses these service names in its configuration to route requests appropriately. ________________________________________ 5. Ensuring Scalability and Fault Tolerance Scalability: • Docker Compose allows you to scale services using the docker-compose up --scale command. Example: To scale the user-service to 3 instances: bash Copy code docker-compose up --scale user-service=3 -d • Express Gateway can handle load balancing across multiple instances of a service. Fault Tolerance: • Health Checks: Implement health check endpoints in each service to monitor their status. Example in user-service and product-service: javascript Copy code // Add to index.js app.get('/health', (req, res) => { res.status(200).send('OK'); }); • Restart Policies: Configure Docker Compose to restart services on failure. yaml Copy code restart: on-failure ________________________________________ 6. Dockerizing the Microservices and API Gateway To streamline deployment and ensure consistency across environments, we'll containerize each microservice and the API Gateway using Docker. 6.1. Create Dockerfile for Each Service a. User Service Dockerfile Create a Dockerfile in user-service/: dockerfile Copy code # user-service/Dockerfile FROM node:14 # Create app directory WORKDIR /usr/src/app # Install app dependencies COPY package*.json ./ RUN npm install # Bundle app source COPY . . EXPOSE 5001 CMD [ "npm", "start" ] b. Product Service Dockerfile Create a Dockerfile in product-service/: dockerfile Copy code # product-service/Dockerfile FROM node:14 # Create app directory WORKDIR /usr/src/app # Install app dependencies COPY package*.json ./ RUN npm install # Bundle app source COPY . . EXPOSE 5002 CMD [ "npm", "start" ] c. API Gateway Dockerfile Create a Dockerfile in api-gateway/: dockerfile Copy code # api-gateway/Dockerfile FROM node:14 # Create app directory WORKDIR /usr/src/app # Install app dependencies COPY package*.json ./ RUN npm install # Bundle app source COPY . . EXPOSE 8080 CMD [ "npm", "start" ] 6.2. Create docker-compose.yml At the root of ibrandidit-microservices/, create a docker-compose.yml file to define all services. yaml Copy code # docker-compose.yml version: '3.8' services: user-service: build: context: ./user-service ports: - '5001:5001' environment: - PORT=5001 - NODE_ENV=development restart: on-failure product-service: build: context: ./product-service ports: - '5002:5002' environment: - PORT=5002 - NODE_ENV=development restart: on-failure api-gateway: build: context: ./api-gateway ports: - '8080:8080' depends_on: - user-service - product-service restart: on-failure Explanation: • user-service: Built from ./user-service, exposes port 5001. • product-service: Built from ./product-service, exposes port 5002. • api-gateway: Built from ./api-gateway, exposes port 8080 and depends on both services. 6.3. Running the Application with Docker Compose 1. Navigate to Project Root bash Copy code cd ibrandidit-microservices/ 2. Build and Start Services bash Copy code docker-compose up --build This command builds Docker images for each service and starts the containers. 3. Accessing Services o User Service: http://localhost:5001 o Product Service: http://localhost:5002 o API Gateway: http://localhost:8080 API Gateway Documentation: Since Express Gateway doesn't have built-in Swagger, access individual service documentation: o User Service API Docs: http://localhost:5001/api-docs o Product Service API Docs: http://localhost:5002/api-docs 4. Testing via API Gateway o List Users: http Copy code GET http://localhost:8080/users o List Products: http Copy code GET http://localhost:8080/products 5. Expected Responses: o Users: json Copy code [ { "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" } ] o Products: json Copy code [ { "id": 1, "name": "T-Shirt", "price": 19.99 }, { "id": 2, "name": "Hoodie", "price": 29.99 } ] ________________________________________ 7. Complete Code and Configuration Files 7.1. Docker Configuration docker-compose.yml yaml Copy code # docker-compose.yml version: '3.8' services: user-service: build: context: ./user-service ports: - '5001:5001' environment: - PORT=5001 - NODE_ENV=development restart: on-failure product-service: build: context: ./product-service ports: - '5002:5002' environment: - PORT=5002 - NODE_ENV=development restart: on-failure api-gateway: build: context: ./api-gateway ports: - '8080:8080' depends_on: - user-service - product-service restart: on-failure Note: Ensure that the docker-compose.yml file is at the root of the ibrandidit-microservices/ directory. 7.2. API Gateway Configuration api-gateway/config/gateway.config.yml yaml Copy code http: port: 8080 apiEndpoints: users: path: /users target: http://user-service:5001 methods: GET, POST userById: path: /users/:id target: http://user-service:5001 methods: GET products: path: /products target: http://product-service:5002 methods: GET, POST productById: path: /products/:id target: http://product-service:5002 methods: GET serviceEndpoints: user-service: url: 'http://user-service:5001' product-service: url: 'http://product-service:5002' policies: - proxy pipelines: usersPipeline: apiEndpoints: - users policies: - proxy: - action: serviceEndpoint: user-service userByIdPipeline: apiEndpoints: - userById policies: - proxy: - action: serviceEndpoint: user-service productsPipeline: apiEndpoints: - products policies: - proxy: - action: serviceEndpoint: product-service productByIdPipeline: apiEndpoints: - productById policies: - proxy: - action: serviceEndpoint: product-service Explanation: • apiEndpoints: Defines public API endpoints that clients can access. • serviceEndpoints: Internal endpoints where microservices are accessible. • policies: Defines the policies applied to each pipeline. Here, we use the proxy policy to route requests. • pipelines: Connects API endpoints to policies. 7.3. User Service Code Refer to the User Service section in Stage 1. Ensure that all files (index.js, swagger.js, routes/userRoutes.js, controllers/userController.js, tests/user.test.js, Dockerfile, etc.) are correctly placed in the user-service/ directory. 7.4. Product Service Code Refer to the Product Service section in Stage 2. Ensure that all files (index.js, swagger.js, routes/productRoutes.js, controllers/productController.js, tests/product.test.js, Dockerfile, etc.) are correctly placed in the product-service/ directory. 7.5. API Gateway Code Refer to the API Gateway section in Stage 2. Ensure that the gateway.config.yml is correctly configured in the api-gateway/config/ directory and that the Dockerfile is correctly placed in the api-gateway/ directory. api-gateway/package.json json Copy code { "name": "api-gateway", "version": "1.0.0", "description": "API Gateway for iBranDIDit.com", "main": "index.js", "scripts": { "start": "express-gateway start" }, "keywords": [], "author": "Your Name", "license": "ISC", "dependencies": { "express-gateway": "^2.30.0" } } api-gateway/Dockerfile dockerfile Copy code # api-gateway/Dockerfile FROM node:14 # Create app directory WORKDIR /usr/src/app # Install Express Gateway globally RUN npm install -g express-gateway # Copy package.json and install dependencies COPY package*.json ./ RUN npm install # Copy gateway configuration COPY . . EXPOSE 8080 CMD [ "express-gateway", "start" ] ________________________________________ 8. Documentation and Usage 8.1. Running the Microservices Locally 1. Ensure Docker and Docker Compose are Installed o Docker Installation: Install Docker o Docker Compose Installation: Install Docker Compose 2. Navigate to Project Root bash Copy code cd ibrandidit-microservices/ 3. Build and Start All Services bash Copy code docker-compose up --build This command builds Docker images for each service and starts the containers. 4. Verify Services are Running o User Service: http://localhost:5001 o Product Service: http://localhost:5002 o API Gateway: http://localhost:8080 5. Access API Documentation o User Service API Docs: http://localhost:5001/api-docs o Product Service API Docs: http://localhost:5002/api-docs o API Gateway: No Swagger documentation; use individual service docs. 8.2. Testing Services via API Gateway • List Users: http Copy code GET http://localhost:8080/users • Get User by ID: http Copy code GET http://localhost:8080/users/1 • Create a New User: http Copy code POST http://localhost:8080/users Content-Type: application/json { "name": "Charlie", "email": "charlie@example.com" } • List Products: http Copy code GET http://localhost:8080/products • Get Product by ID: http Copy code GET http://localhost:8080/products/1 • Create a New Product: http Copy code POST http://localhost:8080/products Content-Type: application/json { "name": "Cap", "price": 9.99 } Expected Responses: • Users: json Copy code [ { "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" } ] • Products: json Copy code [ { "id": 1, "name": "T-Shirt", "price": 19.99 }, { "id": 2, "name": "Hoodie", "price": 29.99 } ] ________________________________________ 9. Additional Configuration 9.1. Health Check Endpoints To monitor the health of each microservice, we've added a /health endpoint in both services. User Service (user-service/index.js): javascript Copy code // Add to index.js app.get('/health', (req, res) => { res.status(200).send('User Service is healthy'); }); Product Service (product-service/index.js): javascript Copy code // Add to index.js app.get('/health', (req, res) => { res.status(200).send('Product Service is healthy'); }); API Gateway Health Check: Express Gateway can be configured to perform health checks if needed, but for simplicity, we're relying on Docker's built-in health check mechanisms. 9.2. Docker Compose Health Checks Modify docker-compose.yml to include health checks for each service. yaml Copy code # docker-compose.yml version: '3.8' services: user-service: build: context: ./user-service ports: - '5001:5001' environment: - PORT=5001 - NODE_ENV=development restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5001/health"] interval: 30s timeout: 10s retries: 3 product-service: build: context: ./product-service ports: - '5002:5002' environment: - PORT=5002 - NODE_ENV=development restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5002/health"] interval: 30s timeout: 10s retries: 3 api-gateway: build: context: ./api-gateway ports: - '8080:8080' depends_on: user-service: condition: service_healthy product-service: condition: service_healthy restart: on-failure healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 Note: Add a /health endpoint to the API Gateway. API Gateway Health Check Endpoint Add the following to api-gateway/config/gateway.config.yml under pipelines: yaml Copy code apiEndpoints: health: path: /health target: http://api-gateway:8080 methods: GET serviceEndpoints: health-endpoint: url: 'http://api-gateway:8080/health' pipelines: healthPipeline: apiEndpoints: - health policies: - proxy: - action: serviceEndpoint: health-endpoint Note: The API Gateway should handle the /health route appropriately. To implement a /health endpoint in the API Gateway, you might need to add a separate policy or a route. However, for simplicity, we'll assume the API Gateway can handle health checks externally. ________________________________________ 10. Running the Complete Microservices Architecture 1. Navigate to Project Root bash Copy code cd ibrandidit-microservices/ 2. Build and Start All Services with Docker Compose bash Copy code docker-compose up --build Output: bash Copy code Creating network "ibrandidit-microservices_default" with the default driver Creating ibrandidit-microservices_user-service_1 ... done Creating ibrandidit-microservices_product-service_1 ... done Creating ibrandidit-microservices_api-gateway_1 ... done 3. Access Services o API Gateway: http://localhost:8080 o User Service: http://localhost:5001 o Product Service: http://localhost:5002 4. Testing via API Gateway o List Users: http Copy code GET http://localhost:8080/users o Get User by ID: http Copy code GET http://localhost:8080/users/1 o Create a New User: http Copy code POST http://localhost:8080/users Content-Type: application/json { "name": "Charlie", "email": "charlie@example.com" } o List Products: http Copy code GET http://localhost:8080/products o Get Product by ID: http Copy code GET http://localhost:8080/products/1 o Create a New Product: http Copy code POST http://localhost:8080/products Content-Type: application/json { "name": "Cap", "price": 9.99 } Expected Responses: • Users: json Copy code [ { "id": 1, "name": "Alice", "email": "alice@example.com" }, { "id": 2, "name": "Bob", "email": "bob@example.com" } ] • Products: json Copy code [ { "id": 1, "name": "T-Shirt", "price": 19.99 }, { "id": 2, "name": "Hoodie", "price": 29.99 } ] • New User Creation: json Copy code { "id": 1697048168403, "name": "Charlie", "email": "charlie@example.com" } • New Product Creation: json Copy code { "id": 1697048168404, "name": "Cap", "price": 9.99 } ________________________________________ 11. Documentation and Usage 11.1. Accessing Individual Service Documentation • User Service: http://localhost:5001/api-docs • Product Service: http://localhost:5002/api-docs 11.2. API Gateway • API Gateway URL: http://localhost:8080 Note: The API Gateway doesn't have its own Swagger documentation. Refer to individual service documentation for detailed API specs. ________________________________________ 12. Troubleshooting Tips 1. Ports Already in Use: o If ports 5001, 5002, or 8080 are in use, stop the conflicting services or change the ports in the respective .env files and docker-compose.yml. 2. Service Unreachable: o Ensure all services are running. Use docker-compose ps to check the status. o Check logs for errors using docker-compose logs [service-name]. 3. API Gateway Not Routing Correctly: o Verify the gateway.config.yml is correctly pointing to the service endpoints. o Ensure services are named correctly in docker-compose.yml and accessible via Docker networking. 4. Health Checks Failing: o Ensure the /health endpoints are correctly implemented in each service. o Verify that services are accessible internally within Docker Compose. ________________________________________ 13. Next Steps With Stage 2 completed, you've successfully transformed your monolithic API into a microservices architecture with an API Gateway. This setup allows for independent scaling, easier maintenance, and better fault isolation. Proceed to Stage 3: Centralized Authentication, where you'll implement a unified authentication system to manage user access across all microservices securely. ________________________________________ 14. Additional Resources • Express Gateway Documentation: https://www.express-gateway.io/docs/ • Docker Compose Documentation: https://docs.docker.com/compose/ • Swagger Documentation: https://swagger.io/docs/ • Jest Documentation: https://jestjs.io/docs/getting-started • Supertest Documentation: https://github.com/visionmedia/supertest ________________________________________ Disclaimer: This guide provides a foundational setup for microservices architecture. Depending on project complexity and requirements, further optimizations and configurations may be necessary.
Stage 3: Centralized Authentication for your eCommerce platform, iBranDIDit.com. This stage introduces a centralized authentication system that manages user registration, login, and secure access to all microservices. We'll implement JWT (JSON Web Tokens) for authentication, create a dedicated Authentication Service, and integrate it with the existing API Gateway and microservices.
Stage 3: Centralized Authentication
________________________________________
1. Overview
Objective:
Implement a centralized authentication system to manage user registration, login, and secure access to all microservices. This includes:
1. Creating an Authentication Service that handles user authentication and JWT issuance.
2. Integrating JWT Authentication into the API Gateway to protect routes.
3. Securing Existing Microservices by validating JWTs in incoming requests.
4. Enhancing User Management with secure password handling.
Benefits:
• Security: Ensures that only authenticated users can access protected resources.
• Scalability: Centralizes authentication, making it easier to manage and scale.
• Maintainability: Simplifies user management across multiple services.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js
• Authentication: JWT (JSON Web Tokens) using jsonwebtoken
• Password Hashing: bcrypt for secure password storage
• API Gateway: Express Gateway (from Stage 2)
• Containerization: Docker
• Orchestration: Docker Compose
• Testing: Jest and Supertest
• Documentation: Swagger (integrated into Authentication Service)
________________________________________
3. Project Structure
We'll extend the existing ibrandidit-microservices project by adding an Authentication Service. The updated structure will look like this:
lua
Copy code
ibrandidit-microservices/
├── api-gateway/
│ ├── config/
│ │ └── gateway.config.yml
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
├── auth-service/
│ ├── controllers/
│ │ └── authController.js
│ ├── routes/
│ │ └── authRoutes.js
│ ├── tests/
│ │ └── auth.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ ├── Dockerfile
│ └── README.md
├── user-service/
│ ├── controllers/
│ │ └── userController.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── tests/
│ │ └── user.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ │ └── productController.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── tests/
│ │ └── product.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
└── README.md
________________________________________
4. Implementation Steps
4.1. Creating the Authentication Service
We'll create a new microservice dedicated to handling authentication.
4.1.1. Create Directory and Initialize Project
bash
Copy code
mkdir -p ibrandidit-microservices/auth-service
cd ibrandidit-microservices/auth-service
npm init -y
4.1.2. Install Dependencies
bash
Copy code
npm install express cors body-parser dotenv jsonwebtoken bcrypt
npm install --save-dev nodemon jest supertest swagger-jsdoc swagger-ui-express
• express: Web framework for Node.js
• cors: Middleware for enabling CORS
• body-parser: Middleware to parse incoming request bodies
• dotenv: Loads environment variables from a .env file
• jsonwebtoken: For creating and verifying JWTs
• bcrypt: For hashing passwords
• nodemon: Utility that monitors for changes and automatically restarts the server
• jest: JavaScript testing framework
• supertest: HTTP assertions for testing
• swagger-jsdoc & swagger-ui-express: Tools for API documentation
4.1.3. Configure package.json Scripts
Update the package.json to include scripts for development, testing, and starting the server.
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
4.1.4. Create Project Files
a. Create .env File
This file will store environment variables.
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
Note: Replace your_jwt_secret_key with a strong, secure secret key in a real application.
b. Create index.js
This is the entry point of your Authentication Service.
javascript
Copy code
// index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes'); // To manage users
// Load environment variables
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Routes
app.use('/auth', authRoutes);
app.use('/users', userRoutes); // Protected route
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Authentication Service API');
});
// Start Server
const PORT = process.env.PORT || 5003;
app.listen(PORT, () => {
console.log(`Authentication Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
c. Create swagger.js
This file sets up Swagger for API documentation.
javascript
Copy code
// swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Authentication Service API',
version: '1.0.0',
description: 'API documentation for the Authentication Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5003',
description: 'Development server',
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
d. Create routes/authRoutes.js
Defines authentication-related routes like registration and login.
javascript
Copy code
// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
/**
* @swagger
* tags:
* name: Authentication
* description: User authentication and registration
*/
/**
* @swagger
* /auth/register:
* post:
* summary: Register a new user
* tags: [Authentication]
* requestBody:
* description: User object that needs to be registered
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RegisterInput'
* responses:
* 201:
* description: User registered successfully
* 400:
* description: Invalid input or user already exists
*/
router.post('/register', authController.register);
/**
* @swagger
* /auth/login:
* post:
* summary: Login a user
* tags: [Authentication]
* requestBody:
* description: User credentials for login
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginInput'
* responses:
* 200:
* description: Login successful, returns JWT
* 400:
* description: Invalid credentials
*/
router.post('/login', authController.login);
module.exports = router;
e. Create controllers/authController.js
Handles registration and login logic.
javascript
Copy code
// controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
/**
* In-memory user store for demonstration purposes.
* In a real application, use a database.
*/
let users = [];
/**
* @swagger
* components:
* schemas:
* RegisterInput:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* name: Alice
* email: alice@example.com
* password: securepassword
* LoginInput:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* email: alice@example.com
* password: securepassword
*/
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
// Check if user already exists
const existingUser = users.find((user) => user.email === email);
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
try {
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = { id: Date.now(), name, email, password: hashedPassword };
users.push(newUser);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser.id, name, email } });
} catch (error) {
res.status(500).json({ message: 'Server error during registration' });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
// Basic validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
try {
// Find user
const user = users.find((user) => user.email === email);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Compare passwords
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
});
res.status(200).json({ message: 'Login successful', token });
} catch (error) {
res.status(500).json({ message: 'Server error during login' });
}
};
f. Create routes/userRoutes.js
Defines user-related routes that are protected and require authentication.
javascript
Copy code
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authenticate = require('../middleware/authenticate');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of users (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of users.
* 401:
* description: Unauthorized
*/
router.get('/', authenticate, userController.getUsers);
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user by ID (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* description: Numeric ID of the user to retrieve
* schema:
* type: integer
* responses:
* 200:
* description: User found
* 401:
* description: Unauthorized
* 404:
* description: User not found
*/
router.get('/:id', authenticate, userController.getUserById);
/**
* @swagger
* /users:
* post:
* summary: Create a new user (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* description: User object that needs to be added
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserInput'
* responses:
* 201:
* description: User created successfully
* 400:
* description: Invalid input
* 401:
* description: Unauthorized
*/
router.post('/', authenticate, userController.createUser);
module.exports = router;
g. Create controllers/userController.js
Handles user-related operations within the Authentication Service.
javascript
Copy code
// controllers/userController.js
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
const jwt = require('jsonwebtoken');
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - id
* - name
* - email
* properties:
* id:
* type: integer
* description: The unique ID of the user
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* example:
* id: 1
* name: Alice
* email: alice@example.com
* UserInput:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* name: Alice
* email: alice@example.com
* password: securepassword
*/
let users = []; // This should be replaced with a database in production
exports.getUsers = (req, res) => {
// In a real application, fetch users from the database
const usersToReturn = users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
}));
res.status(200).json(usersToReturn);
};
exports.getUserById = (req, res) => {
const userId = parseInt(req.params.id);
const user = users.find((user) => user.id === userId);
if (user) {
const { id, name, email } = user;
res.status(200).json({ id, name, email });
} else {
res.status(404).json({ message: 'User not found' });
}
};
exports.createUser = (req, res) => {
const { name, email, password } = req.body;
// In a real application, add the user to the database
const newUser = { id: Date.now(), name, email, password };
users.push(newUser);
res.status(201).json({ id: newUser.id, name, email });
};
h. Create middleware/authenticate.js
Middleware to verify JWTs in protected routes.
javascript
Copy code
// middleware/authenticate.js
const jwt = require('jsonwebtoken');
module.exports = function (req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
i. Create tests/auth.test.js
Tests for the Authentication Service.
javascript
Copy code
// tests/auth.test.js
const request = require('supertest');
const app = require('../index');
describe('Authentication API Endpoints', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'User registered successfully');
expect(res.body.user).toHaveProperty('id');
expect(res.body.user).toHaveProperty('name', 'Charlie');
expect(res.body.user).toHaveProperty('email', 'charlie@example.com');
});
it('should not register a user with existing email', async () => {
// Register the same user again
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User already exists');
});
it('should login an existing user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Login successful');
expect(res.body).toHaveProperty('token');
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'wrongpassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should not login a non-existent user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should retrieve a list of users with valid token', async () => {
// First, login to get the token
const loginRes = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'securepassword',
});
const token = loginRes.body.token;
// Access protected route
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Charlie');
});
it('should not retrieve users without token', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Access token missing');
});
});
j. Create Dockerfile
Containerize the Authentication Service.
dockerfile
Copy code
# auth-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5003
CMD [ "npm", "start" ]
4.1.5. Testing the Authentication Service
1. Run Tests
bash
Copy code
npm test
You should see output indicating that all tests have passed.
2. Start the Authentication Service
bash
Copy code
npm run dev
You should see:
arduino
Copy code
Authentication Service running in development mode on port 5003
3. Access Authentication Service API Documentation
Open your browser and navigate to: http://localhost:5003/api-docs
You'll see the Swagger UI with documentation for your Authentication Service API.
________________________________________
4.2. Integrating Authentication with the API Gateway
To protect routes in the API Gateway, we'll implement JWT validation at the gateway level. However, Express Gateway does not natively handle JWT authentication out of the box, so we'll use policies and middleware to achieve this.
4.2.1. Update API Gateway Configuration
Edit api-gateway/config/gateway.config.yml
We'll add an authentication policy to verify JWTs before routing requests to microservices.
yaml
Copy code
# api-gateway/config/gateway.config.yml
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- apiKeyAuth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- apiKeyAuth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Should match JWT_SECRET in auth-service
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
Explanation:
• jwt-auth Policy: Validates JWTs using the provided secret key.
• authentication: Specifies which API endpoints require authentication.
• pipelines: Defines the order of policies. For protected routes (users), it first applies jwt-auth and then proxies the request to the respective service.
Note: Replace 'your_jwt_secret_key' with the actual JWT_SECRET used in the Authentication Service. To keep secrets secure, consider using environment variables or a secrets manager in production.
4.2.2. Implement JWT Authentication Policy
Express Gateway allows custom policies. We'll create a custom policy for JWT authentication.
1. Create Policy Directory
bash
Copy code
mkdir -p api-gateway/policies/jwt-auth
2. Create jwt-auth.js
javascript
Copy code
// api-gateway/policies/jwt-auth/jwt-auth.js
const jwt = require('jsonwebtoken');
module.exports = {
name: 'jwt-auth',
policy: (actionParams) => {
return (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, actionParams.secretOrPublicKey, { algorithms: actionParams.algorithms }, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
},
schema: {
secretOrPublicKey: {
type: 'string',
required: true,
},
algorithms: {
type: 'array',
items: { type: 'string' },
required: true,
},
},
};
3. Update package.json
Ensure Express Gateway recognizes the custom policy by adding it to the package.json.
json
Copy code
{
"name": "api-gateway",
"version": "1.0.0",
"description": "API Gateway for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "express-gateway start"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express-gateway": "^2.30.0",
"jsonwebtoken": "^9.0.0"
}
}
Note: Install jsonwebtoken in the API Gateway to handle JWT verification.
bash
Copy code
cd api-gateway
npm install jsonwebtoken
4. Restart API Gateway
After adding the custom policy, rebuild and restart the API Gateway.
bash
Copy code
cd ../
docker-compose up --build
4.2.3. Update Authentication Service
Ensure the Authentication Service can handle registration and login, issuing JWTs that the API Gateway can verify.
No additional changes are needed here since the Authentication Service already issues JWTs using the same JWT_SECRET used by the API Gateway.
________________________________________
4.3. Securing Existing Microservices
To ensure that only authenticated requests reach the microservices, no changes are required in the microservices themselves since the API Gateway handles JWT verification. However, if microservices need to access user information, you can pass the user data from the JWT in the request headers.
Example:
• In the API Gateway's jwt-auth policy, after verifying the JWT, you can add user information to the request headers.
Modify the jwt-auth.js policy:
javascript
Copy code
// api-gateway/policies/jwt-auth/jwt-auth.js
const jwt = require('jsonwebtoken');
module.exports = {
name: 'jwt-auth',
policy: (actionParams) => {
return (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, actionParams.secretOrPublicKey, { algorithms: actionParams.algorithms }, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
// Add user info to headers for downstream services
req.headers['x-user-id'] = user.id;
req.headers['x-user-email'] = user.email;
next();
});
};
},
schema: {
secretOrPublicKey: {
type: 'string',
required: true,
},
algorithms: {
type: 'array',
items: { type: 'string' },
required: true,
},
},
};
In Microservices:
If needed, access the user information from the headers.
javascript
Copy code
// Example in user-service/controllers/userController.js
exports.getUsers = (req, res) => {
const requestingUserId = req.headers['x-user-id'];
const requestingUserEmail = req.headers['x-user-email'];
// Use requestingUserId or requestingUserEmail as needed
// For demonstration, we'll just log them
console.log(`Request made by User ID: ${requestingUserId}, Email: ${requestingUserEmail}`);
// Proceed with fetching users
const usersToReturn = users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
}));
res.status(200).json(usersToReturn);
};
Note: Always validate and sanitize any user-provided data in microservices.
________________________________________
5. Dockerizing the Authentication Service
To integrate the Authentication Service into the existing Docker Compose setup, ensure it is properly configured.
5.1. Create docker-compose.yml
Update the root docker-compose.yml to include the auth-service.
yaml
Copy code
# docker-compose.yml
version: '3.8'
services:
auth-service:
build:
context: ./auth-service
ports:
- '5003:5003'
environment:
- PORT=5003
- NODE_ENV=development
- JWT_SECRET=your_jwt_secret_key # Should match API Gateway's config
- JWT_EXPIRES_IN=1h
restart: on-failure
user-service:
build:
context: ./user-service
ports:
- '5001:5001'
environment:
- PORT=5001
- NODE_ENV=development
restart: on-failure
depends_on:
- auth-service
networks:
- backend
product-service:
build:
context: ./product-service
ports:
- '5002:5002'
environment:
- PORT=5002
- NODE_ENV=development
restart: on-failure
depends_on:
- auth-service
networks:
- backend
api-gateway:
build:
context: ./api-gateway
ports:
- '8080:8080'
depends_on:
- user-service
- product-service
- auth-service
restart: on-failure
networks:
- backend
networks:
backend:
driver: bridge
Explanation:
• auth-service: Added with its environment variables, including JWT_SECRET which must match the one used in the API Gateway.
• user-service & product-service: Depend on auth-service to ensure it starts first.
• networks: All services are connected to the backend network for internal communication.
Note: Replace your_jwt_secret_key with the actual secret key used in the Authentication Service.
5.2. Restart Docker Compose
Rebuild and restart all services to include the Authentication Service.
bash
Copy code
cd ibrandidit-microservices/
docker-compose up --build
Output:
bash
Copy code
Creating network "ibrandidit-microservices_backend" with the default driver
Creating ibrandidit-microservices_auth-service_1 ... done
Creating ibrandidit-microservices_user-service_1 ... done
Creating ibrandidit-microservices_product-service_1 ... done
Creating ibrandidit-microservices_api-gateway_1 ... done
________________________________________
6. Testing the Centralized Authentication Flow
6.1. Register a New User
http
Copy code
POST http://localhost:5003/auth/register
Content-Type: application/json
{
"name": "David",
"email": "david@example.com",
"password": "strongpassword"
}
Expected Response:
json
Copy code
{
"message": "User registered successfully",
"user": {
"id": 1697048168403,
"name": "David",
"email": "david@example.com"
}
}
6.2. Login with Registered User
http
Copy code
POST http://localhost:5003/auth/login
Content-Type: application/json
{
"email": "david@example.com",
"password": "strongpassword"
}
Expected Response:
json
Copy code
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}
6.3. Access Protected Route via API Gateway
Use the received JWT to access protected routes.
a. List Users
http
Copy code
GET http://localhost:8080/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Expected Response:
json
Copy code
[
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com"
},
{
"id": 1697048168403,
"name": "Charlie",
"email": "charlie@example.com"
}
]
b. Create a New User (Protected Route)
http
Copy code
POST http://localhost:8080/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"name": "Eve",
"email": "eve@example.com",
"password": "anotherstrongpassword"
}
Expected Response:
json
Copy code
{
"id": 1697048168404,
"name": "Eve",
"email": "eve@example.com"
}
6.4. Access Without Token
Attempt to access a protected route without a token.
http
Copy code
GET http://localhost:8080/users
Expected Response:
json
Copy code
{
"message": "Access token missing"
}
________________________________________
7. Complete Code and Configuration Files
7.1. Authentication Service Code
auth-service/package.json
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
auth-service/.env
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
Note: Replace your_jwt_secret_key with a secure, randomly generated string.
auth-service/index.js
javascript
Copy code
// auth-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes'); // To manage users
// Load environment variables
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Routes
app.use('/auth', authRoutes);
app.use('/users', userRoutes); // Protected route
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Authentication Service API');
});
// Start Server
const PORT = process.env.PORT || 5003;
app.listen(PORT, () => {
console.log(`Authentication Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
auth-service/swagger.js
javascript
Copy code
// auth-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Authentication Service API',
version: '1.0.0',
description: 'API documentation for the Authentication Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5003',
description: 'Development server',
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
auth-service/routes/authRoutes.js
javascript
Copy code
// auth-service/routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
/**
* @swagger
* tags:
* name: Authentication
* description: User authentication and registration
*/
/**
* @swagger
* /auth/register:
* post:
* summary: Register a new user
* tags: [Authentication]
* requestBody:
* description: User object that needs to be registered
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RegisterInput'
* responses:
* 201:
* description: User registered successfully
* 400:
* description: Invalid input or user already exists
*/
router.post('/register', authController.register);
/**
* @swagger
* /auth/login:
* post:
* summary: Login a user
* tags: [Authentication]
* requestBody:
* description: User credentials for login
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginInput'
* responses:
* 200:
* description: Login successful, returns JWT
* 400:
* description: Invalid credentials
*/
router.post('/login', authController.login);
module.exports = router;
auth-service/controllers/authController.js
javascript
Copy code
// auth-service/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
/**
* In-memory user store for demonstration purposes.
* In a real application, use a database.
*/
let users = [];
/**
* @swagger
* components:
* schemas:
* RegisterInput:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* name: Alice
* email: alice@example.com
* password: securepassword
* LoginInput:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* email: alice@example.com
* password: securepassword
*/
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
// Check if user already exists
const existingUser = users.find((user) => user.email === email);
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
try {
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = { id: Date.now(), name, email, password: hashedPassword };
users.push(newUser);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser.id, name, email } });
} catch (error) {
res.status(500).json({ message: 'Server error during registration' });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
// Basic validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
try {
// Find user
const user = users.find((user) => user.email === email);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Compare passwords
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
});
res.status(200).json({ message: 'Login successful', token });
} catch (error) {
res.status(500).json({ message: 'Server error during login' });
}
};
auth-service/routes/userRoutes.js
javascript
Copy code
// auth-service/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authenticate = require('../middleware/authenticate');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of users (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of users.
* 401:
* description: Unauthorized
*/
router.get('/', authenticate, userController.getUsers);
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user by ID (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* description: Numeric ID of the user to retrieve
* schema:
* type: integer
* responses:
* 200:
* description: User found
* 401:
* description: Unauthorized
* 404:
* description: User not found
*/
router.get('/:id', authenticate, userController.getUserById);
/**
* @swagger
* /users:
* post:
* summary: Create a new user (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* description: User object that needs to be added
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserInput'
* responses:
* 201:
* description: User created successfully
* 400:
* description: Invalid input
* 401:
* description: Unauthorized
*/
router.post('/', authenticate, userController.createUser);
module.exports = router;
auth-service/controllers/userController.js
javascript
Copy code
// auth-service/controllers/userController.js
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
const jwt = require('jsonwebtoken');
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - id
* - name
* - email
* properties:
* id:
* type: integer
* description: The unique ID of the user
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* example:
* id: 1
* name: Alice
* email: alice@example.com
* UserInput:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* name: Alice
* email: alice@example.com
* password: securepassword
*/
let users = []; // This should be replaced with a database in production
exports.getUsers = (req, res) => {
// In a real application, fetch users from the database
const usersToReturn = users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
}));
res.status(200).json(usersToReturn);
};
exports.getUserById = (req, res) => {
const userId = parseInt(req.params.id);
const user = users.find((user) => user.id === userId);
if (user) {
const { id, name, email } = user;
res.status(200).json({ id, name, email });
} else {
res.status(404).json({ message: 'User not found' });
}
};
exports.createUser = (req, res) => {
const { name, email, password } = req.body;
// In a real application, add the user to the database
const newUser = { id: Date.now(), name, email, password };
users.push(newUser);
res.status(201).json({ id: newUser.id, name, email });
};
auth-service/middleware/authenticate.js
javascript
Copy code
// auth-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
module.exports = function (req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
auth-service/tests/auth.test.js
javascript
Copy code
// auth-service/tests/auth.test.js
const request = require('supertest');
const app = require('../index');
describe('Authentication API Endpoints', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'User registered successfully');
expect(res.body.user).toHaveProperty('id');
expect(res.body.user).toHaveProperty('name', 'Charlie');
expect(res.body.user).toHaveProperty('email', 'charlie@example.com');
});
it('should not register a user with existing email', async () => {
// Register the same user again
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User already exists');
});
it('should login an existing user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Login successful');
expect(res.body).toHaveProperty('token');
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'wrongpassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should not login a non-existent user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should retrieve a list of users with valid token', async () => {
// First, register and login to get the token
await request(app)
.post('/auth/register')
.send({
name: 'Eve',
email: 'eve@example.com',
password: 'strongpassword',
});
const loginRes = await request(app)
.post('/auth/login')
.send({
email: 'eve@example.com',
password: 'strongpassword',
});
const token = loginRes.body.token;
// Access protected route
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Charlie');
expect(res.body[1]).toHaveProperty('name', 'Eve');
});
it('should not retrieve users without token', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Access token missing');
});
});
auth-service/Dockerfile
dockerfile
Copy code
# auth-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5003
CMD [ "npm", "start" ]
7.2. API Gateway Code
api-gateway/package.json
json
Copy code
{
"name": "api-gateway",
"version": "1.0.0",
"description": "API Gateway for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "express-gateway start"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express-gateway": "^2.30.0",
"jsonwebtoken": "^9.0.0"
}
}
api-gateway/policies/jwt-auth/jwt-auth.js
javascript
Copy code
// api-gateway/policies/jwt-auth/jwt-auth.js
const jwt = require('jsonwebtoken');
module.exports = {
name: 'jwt-auth',
policy: (actionParams) => {
return (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, actionParams.secretOrPublicKey, { algorithms: actionParams.algorithms }, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
// Add user info to headers for downstream services
req.headers['x-user-id'] = user.id;
req.headers['x-user-email'] = user.email;
next();
});
};
},
schema: {
secretOrPublicKey: {
type: 'string',
required: true,
},
algorithms: {
type: 'array',
items: { type: 'string' },
required: true,
},
},
};
api-gateway/config/gateway.config.yml
yaml
Copy code
# api-gateway/config/gateway.config.yml
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- jwt-auth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- jwt-auth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
Important:
• Replace 'your_jwt_secret_key' with the actual JWT_SECRET from the Authentication Service (auth-service/.env).
api-gateway/Dockerfile
dockerfile
Copy code
# api-gateway/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install Express Gateway globally
RUN npm install -g express-gateway
# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install
# Copy gateway configuration and policies
COPY . .
EXPOSE 8080
CMD [ "express-gateway", "start" ]
7.3. User Service Code
No changes needed for the User Service as the API Gateway handles JWT verification and passes user information via headers.
7.4. Product Service Code
No changes needed for the Product Service unless you want to utilize the passed user information.
________________________________________
8. Dockerizing and Integrating the Authentication Service
8.1. Update docker-compose.yml
Ensure the Authentication Service is included and connected to the same network as other services.
yaml
Copy code
# docker-compose.yml
version: '3.8'
services:
auth-service:
build:
context: ./auth-service
ports:
- '5003:5003'
environment:
- PORT=5003
- NODE_ENV=development
- JWT_SECRET=your_jwt_secret_key
- JWT_EXPIRES_IN=1h
restart: on-failure
networks:
- backend
user-service:
build:
context: ./user-service
ports:
- '5001:5001'
environment:
- PORT=5001
- NODE_ENV=development
restart: on-failure
depends_on:
- auth-service
networks:
- backend
product-service:
build:
context: ./product-service
ports:
- '5002:5002'
environment:
- PORT=5002
- NODE_ENV=development
restart: on-failure
depends_on:
- auth-service
networks:
- backend
api-gateway:
build:
context: ./api-gateway
ports:
- '8080:8080'
depends_on:
- user-service
- product-service
- auth-service
restart: on-failure
networks:
- backend
networks:
backend:
driver: bridge
Ensure that JWT_SECRET in auth-service matches the one in the API Gateway configuration.
8.2. Rebuild and Restart Services
bash
Copy code
cd ibrandidit-microservices/
docker-compose up --build
Output:
bash
Copy code
Creating network "ibrandidit-microservices_backend" with the default driver
Creating ibrandidit-microservices_auth-service_1 ... done
Creating ibrandidit-microservices_user-service_1 ... done
Creating ibrandidit-microservices_product-service_1 ... done
Creating ibrandidit-microservices_api-gateway_1 ... done
________________________________________
9. Documentation and Usage
9.1. Accessing Authentication Service API Documentation
Open your browser and navigate to: http://localhost:5003/api-docs
9.2. Authentication Workflow
1. Register a New User:
http
Copy code
POST http://localhost:5003/auth/register
Content-Type: application/json
{
"name": "David",
"email": "david@example.com",
"password": "strongpassword"
}
2. Login with Registered User:
http
Copy code
POST http://localhost:5003/auth/login
Content-Type: application/json
{
"email": "david@example.com",
"password": "strongpassword"
}
Response:
json
Copy code
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}
3. Access Protected Routes via API Gateway:
Use the received JWT to access protected endpoints.
o List Users:
http
Copy code
GET http://localhost:8080/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
o Create a New User:
http
Copy code
POST http://localhost:8080/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"name": "Eve",
"email": "eve@example.com",
"password": "anotherstrongpassword"
}
9.3. Accessing Swagger Documentation
Each microservice has its own Swagger documentation:
• Authentication Service: http://localhost:5003/api-docs
• User Service: http://localhost:5001/api-docs
• Product Service: http://localhost:5002/api-docs
9.4. Handling Protected Routes
• API Gateway uses the jwt-auth policy to verify JWTs.
• If a valid JWT is provided, the request is proxied to the respective microservice.
• If not, an error message is returned (401 or 403).
________________________________________
10. Complete Code and Configuration Files
10.1. auth-service/package.json
(Already provided above)
10.2. auth-service/.env
(Already provided above)
10.3. auth-service/index.js
(Already provided above)
10.4. auth-service/swagger.js
(Already provided above)
10.5. auth-service/routes/authRoutes.js
(Already provided above)
10.6. auth-service/controllers/authController.js
(Already provided above)
10.7. auth-service/routes/userRoutes.js
(Already provided above)
10.8. auth-service/controllers/userController.js
(Already provided above)
10.9. auth-service/middleware/authenticate.js
(Already provided above)
10.10. auth-service/tests/auth.test.js
(Already provided above)
10.11. auth-service/Dockerfile
(Already provided above)
10.12. api-gateway/package.json
(Already provided above)
10.13. api-gateway/policies/jwt-auth/jwt-auth.js
(Already provided above)
10.14. api-gateway/config/gateway.config.yml
(Already provided above)
10.15. api-gateway/Dockerfile
(Already provided above)
10.16. docker-compose.yml
(Already provided above)
________________________________________
11. Summary and Next Steps
11.1. Summary
• Authentication Service: Implemented a dedicated service handling user registration and login, issuing JWTs upon successful authentication.
• API Gateway Integration: Configured the API Gateway to protect routes using the jwt-auth policy, verifying JWTs before routing requests to microservices.
• Microservices Security: Ensured that only authenticated requests reach protected routes in the User Service. The Product Service remains unprotected, assuming it's publicly accessible or requires separate protection.
• Testing: Set up comprehensive tests for the Authentication Service to ensure reliable functionality.
11.2. Next Steps
With Stage 3 completed, you're now ready to proceed to Stage 4: Event-Driven Communication. This stage will involve setting up real-time communication between microservices using messaging queues, enabling features like real-time notifications, order processing updates, and more.
________________________________________
12. Additional Resources
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• JSON Web Tokens (JWT): https://jwt.io/introduction/
• JWT Best Practices: https://auth0.com/docs/security/tokens/json-web-tokens/json-web-token-best-practices
• Swagger Documentation: https://swagger.io/docs/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
________________________________________
13. Troubleshooting Tips
1. JWT Secret Mismatch:
o Ensure that the JWT_SECRET in the Authentication Service matches the secretOrPublicKey in the API Gateway's gateway.config.yml.
o Both should use the same secret key for JWT verification.
2. API Gateway Not Protecting Routes:
o Verify that the jwt-auth policy is correctly applied to the desired API endpoints in gateway.config.yml.
o Ensure that the custom jwt-auth policy is correctly implemented and included in the package.json.
3. Failed JWT Verification:
o Check if the JWT is correctly signed with the secret key.
o Ensure the JWT hasn't expired.
4. CORS Issues:
o Ensure that the Authentication Service and other microservices have CORS enabled for the necessary origins.
5. Service Communication Issues:
o Verify that all services are connected to the same Docker network (backend in this case).
o Check Docker Compose logs for any errors using:
bash
Copy code
docker-compose logs [service-name]
6. Swagger UI Not Loading:
o Ensure that Swagger is correctly set up in each microservice.
o Verify that the apis path in swagger.js correctly points to your route and controller files.
7. Testing Failures:
o Ensure that the Authentication Service is running before running tests.
o Check for typos or mismatched route paths in test cases.
________________________________________
By following this comprehensive guide for Stage 3: Centralized Authentication, you've successfully implemented a secure, centralized authentication system for your microservices architecture. This setup ensures that only authenticated users can access protected resources, laying a solid foundation for building more complex and secure features in subsequent stages.
Stage 4: Event-Driven Communication for your eCommerce platform, iBranDIDit.com. In this stage, we'll introduce an event-driven architecture using RabbitMQ as the messaging broker. This will enable your microservices to communicate asynchronously, enhancing scalability, reliability, and responsiveness.
________________________________________
Stage 4: Event-Driven Communication
________________________________________
1. Overview
Objective:
Implement an event-driven architecture to facilitate asynchronous communication between microservices. This involves:
1. Introducing a Message Broker: Set up RabbitMQ to handle message passing.
2. Publishing Events: Enable microservices to publish events when significant actions occur (e.g., user registration, product creation).
3. Subscribing to Events: Allow microservices to listen for and react to relevant events.
4. Creating a Notification Service: (Optional) Implement a service that handles specific events, such as sending welcome emails or updating inventories.
Benefits:
• Decoupling: Services operate independently without tight integrations.
• Scalability: Easily scale individual services based on demand.
• Reliability: Ensure messages are delivered even if some services are temporarily unavailable.
• Responsiveness: Enhance user experience by handling tasks asynchronously.
________________________________________
2. Technology Stack
• Message Broker: RabbitMQ
• Backend Framework: Node.js with Express.js
• Containerization: Docker
• Orchestration: Docker Compose
• Libraries:
o amqplib: RabbitMQ client for Node.js
• Documentation: Swagger (integrated into existing services)
• Testing: Jest and Supertest
________________________________________
3. Project Structure
We'll extend the existing ibrandidit-microservices project by adding a Notification Service and integrating RabbitMQ for event-driven communication.
lua
Copy code
ibrandidit-microservices/
├── api-gateway/
│ ├── config/
│ │ └── gateway.config.yml
│ ├── policies/
│ │ └── jwt-auth/
│ │ └── jwt-auth.js
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
├── auth-service/
│ ├── controllers/
│ │ └── authController.js
│ ├── middleware/
│ │ └── authenticate.js
│ ├── routes/
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── tests/
│ │ └── auth.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ ├── Dockerfile
│ └── README.md
├── user-service/
│ ├── controllers/
│ │ └── userController.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── tests/
│ │ └── user.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ │ └── productController.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── tests/
│ │ └── product.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── notification-service/
│ ├── controllers/
│ │ └── notificationController.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── notification.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ ├── Dockerfile
│ └── README.md
├── docker-compose.yml
└── README.md
________________________________________
4. Implementation Steps
4.1. Setting Up RabbitMQ
RabbitMQ will act as the central message broker facilitating communication between microservices.
4.1.1. Update docker-compose.yml
Add RabbitMQ to your Docker Compose setup.
yaml
Copy code
# docker-compose.yml
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- '5672:5672' # RabbitMQ server
- '15672:15672' # RabbitMQ Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
networks:
- backend
auth-service:
build:
context: ./auth-service
ports:
- '5003:5003'
environment:
- PORT=5003
- NODE_ENV=development
- JWT_SECRET=your_jwt_secret_key
- JWT_EXPIRES_IN=1h
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- rabbitmq
networks:
- backend
user-service:
build:
context: ./user-service
ports:
- '5001:5001'
environment:
- PORT=5001
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- auth-service
- rabbitmq
networks:
- backend
product-service:
build:
context: ./product-service
ports:
- '5002:5002'
environment:
- PORT=5002
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- auth-service
- rabbitmq
networks:
- backend
notification-service:
build:
context: ./notification-service
ports:
- '5004:5004'
environment:
- PORT=5004
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- rabbitmq
networks:
- backend
api-gateway:
build:
context: ./api-gateway
ports:
- '8080:8080'
depends_on:
- user-service
- product-service
- auth-service
- rabbitmq
restart: on-failure
networks:
- backend
networks:
backend:
driver: bridge
Explanation:
• rabbitmq:
o image: Uses RabbitMQ with the management plugin for easy monitoring.
o ports: Exposes RabbitMQ server on 5672 and Management UI on 15672.
o environment: Sets default credentials.
o networks: Connects to the backend network for inter-service communication.
• Other Services:
o RABBITMQ_URL: Environment variable specifying the RabbitMQ connection string.
o depends_on: Ensures services start after RabbitMQ is up.
o notification-service: New service to handle notifications based on events.
4.2. Implementing Event Publishing in Existing Services
We'll enable the auth-service and product-service to publish events to RabbitMQ when significant actions occur.
4.2.1. Install amqplib
All services that interact with RabbitMQ need the amqplib library.
bash
Copy code
# Navigate to each service and install amqplib
cd auth-service
npm install amqplib
cd ../product-service
npm install amqplib
cd ../user-service
npm install amqplib
cd ../api-gateway
npm install amqplib
cd ../notification-service
npm install amqplib
cd ../
4.2.2. Create a RabbitMQ Service in Each Publisher
We'll create a utility module to handle RabbitMQ connections and event publishing.
Example for auth-service:
a. Create services/rabbitmqService.js
javascript
Copy code
// auth-service/services/rabbitmqService.js
const amqp = require('amqplib');
let channel, connection;
const connectRabbitMQ = async () => {
try {
connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
console.log('Connected to RabbitMQ');
} catch (error) {
console.error('Failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const publishEvent = async (queue, message) => {
try {
await channel.assertQueue(queue, { durable: true });
channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
persistent: true,
});
console.log(`Published message to ${queue}:`, message);
} catch (error) {
console.error('Failed to publish message:', error);
}
};
connectRabbitMQ();
module.exports = {
publishEvent,
};
Explanation:
• connectRabbitMQ: Establishes a connection to RabbitMQ and creates a channel.
• publishEvent: Publishes messages to a specified queue.
• Retry Logic: Attempts to reconnect every 5 seconds if the connection fails.
b. Update auth-service/controllers/authController.js
Modify the register method to publish a user_registered event.
javascript
Copy code
// auth-service/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rabbitmqService = require('../services/rabbitmqService');
let users = [];
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
// Check if user already exists
const existingUser = users.find((user) => user.email === email);
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
try {
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = { id: Date.now(), name, email, password: hashedPassword };
users.push(newUser);
// Publish event to RabbitMQ
const event = {
event: 'user_registered',
data: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
},
};
await rabbitmqService.publishEvent('user_events', event);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser.id, name, email } });
} catch (error) {
res.status(500).json({ message: 'Server error during registration' });
}
};
Explanation:
• After successfully registering a user, an event named user_registered is published to the user_events queue with user details.
4.3. Implementing Event Subscription in Notification Service
The notification-service will listen for events and handle them accordingly (e.g., sending welcome emails).
4.3.1. Create notification-service/services/rabbitmqService.js
javascript
Copy code
// notification-service/services/rabbitmqService.js
const amqp = require('amqplib');
let channel, connection;
const connectRabbitMQ = async () => {
try {
connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
await channel.assertQueue('user_events', { durable: true });
console.log('Connected to RabbitMQ and asserted user_events queue');
startConsuming();
} catch (error) {
console.error('Failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const startConsuming = () => {
channel.consume('user_events', (msg) => {
if (msg !== null) {
const event = JSON.parse(msg.content.toString());
handleEvent(event);
channel.ack(msg);
}
});
};
const handleEvent = (event) => {
switch (event.event) {
case 'user_registered':
sendWelcomeEmail(event.data);
break;
// Add more cases for different events
default:
console.log('Unhandled event:', event);
}
};
const sendWelcomeEmail = (user) => {
// Simulate sending an email
console.log(`Sending welcome email to ${user.email}`);
// Integrate with an email service like SendGrid or Nodemailer in production
};
connectRabbitMQ();
module.exports = {};
Explanation:
• connectRabbitMQ: Connects to RabbitMQ and asserts the user_events queue.
• startConsuming: Listens for messages on the user_events queue.
• handleEvent: Processes incoming events. Currently handles user_registered events by simulating sending a welcome email.
4.3.2. Create notification-service/controllers/notificationController.js
(Optional) If you plan to expose APIs for notifications.
javascript
Copy code
// notification-service/controllers/notificationController.js
// Currently, notifications are handled internally via RabbitMQ events.
// This controller can be expanded if you want to expose notification-related APIs.
4.3.3. Create notification-service/routes/notificationRoutes.js
(Optional) If you plan to expose APIs for notifications.
javascript
Copy code
// notification-service/routes/notificationRoutes.js
const express = require('express');
const router = express.Router();
const notificationController = require('../controllers/notificationController');
// Define notification-related routes here
module.exports = router;
4.3.4. Create notification-service/index.js
javascript
Copy code
// notification-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const notificationRoutes = require('./routes/notificationRoutes');
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Routes
app.use('/notifications', notificationRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Notification Service API');
});
// Start Server
const PORT = process.env.PORT || 5004;
app.listen(PORT, () => {
console.log(`Notification Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app;
4.3.5. Create notification-service/swagger.js
javascript
Copy code
// notification-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Notification Service API',
version: '1.0.0',
description: 'API documentation for the Notification Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5004',
description: 'Development server',
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
4.3.6. Create notification-service/package.json
json
Copy code
{
"name": "notification-service",
"version": "1.0.0",
"description": "Notification Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
4.3.7. Create notification-service/tests/notification.test.js
javascript
Copy code
// notification-service/tests/notification.test.js
const request = require('supertest');
const app = require('../index');
describe('Notification Service API Endpoints', () => {
it('should return welcome message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toEqual(200);
expect(res.text).toBe('Welcome to Notification Service API');
});
// Additional tests can be added here as the service evolves
});
4.3.8. Create notification-service/Dockerfile
dockerfile
Copy code
# notification-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5004
CMD [ "npm", "start" ]
4.4. Updating Existing Services to Publish Events
We'll update the product-service to publish a product_created event when a new product is added.
4.4.1. Update product-service/services/rabbitmqService.js
Create a RabbitMQ service for the product-service.
javascript
Copy code
// product-service/services/rabbitmqService.js
const amqp = require('amqplib');
let channel, connection;
const connectRabbitMQ = async () => {
try {
connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
console.log('Product Service connected to RabbitMQ');
} catch (error) {
console.error('Product Service failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const publishEvent = async (queue, message) => {
try {
await channel.assertQueue(queue, { durable: true });
channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
persistent: true,
});
console.log(`Product Service published message to ${queue}:`, message);
} catch (error) {
console.error('Product Service failed to publish message:', error);
}
};
connectRabbitMQ();
module.exports = {
publishEvent,
};
4.4.2. Update product-service/controllers/productController.js
Modify the createProduct method to publish a product_created event.
javascript
Copy code
// product-service/controllers/productController.js
const rabbitmqService = require('../services/rabbitmqService');
let products = [];
exports.getProducts = (req, res) => {
res.status(200).json(products);
};
exports.getProductById = (req, res) => {
const productId = parseInt(req.params.id);
const product = products.find((prod) => prod.id === productId);
if (product) {
res.status(200).json(product);
} else {
res.status(404).json({ message: 'Product not found' });
}
};
exports.createProduct = async (req, res) => {
const { name, price } = req.body;
if (name && price) {
const newProduct = { id: Date.now(), name, price };
products.push(newProduct);
// Publish event to RabbitMQ
const event = {
event: 'product_created',
data: {
id: newProduct.id,
name: newProduct.name,
price: newProduct.price,
},
};
await rabbitmqService.publishEvent('product_events', event);
res.status(201).json(newProduct);
} else {
res.status(400).json({ message: 'Name and price are required' });
}
};
Explanation:
• After successfully creating a product, a product_created event is published to the product_events queue with product details.
4.5. Creating the Notification Service
The notification-service listens to events from RabbitMQ and handles them accordingly.
4.5.1. Implementing Event Handling
As previously shown, the notification-service listens to the user_events queue and handles user_registered events. Similarly, it can listen to the product_events queue for product_created events.
Update notification-service/services/rabbitmqService.js
Modify the service to listen to multiple queues.
javascript
Copy code
// notification-service/services/rabbitmqService.js
const amqp = require('amqplib');
let channel, connection;
const connectRabbitMQ = async () => {
try {
connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
await channel.assertQueue('user_events', { durable: true });
await channel.assertQueue('product_events', { durable: true });
console.log('Notification Service connected to RabbitMQ and asserted queues');
startConsuming();
} catch (error) {
console.error('Notification Service failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const startConsuming = () => {
// Consume user_events
channel.consume('user_events', (msg) => {
if (msg !== null) {
const event = JSON.parse(msg.content.toString());
handleEvent(event);
channel.ack(msg);
}
});
// Consume product_events
channel.consume('product_events', (msg) => {
if (msg !== null) {
const event = JSON.parse(msg.content.toString());
handleEvent(event);
channel.ack(msg);
}
});
};
const handleEvent = (event) => {
switch (event.event) {
case 'user_registered':
sendWelcomeEmail(event.data);
break;
case 'product_created':
updateInventory(event.data);
break;
// Add more cases for different events
default:
console.log('Unhandled event:', event);
}
};
const sendWelcomeEmail = (user) => {
// Simulate sending an email
console.log(`Sending welcome email to ${user.email}`);
// Integrate with an email service like SendGrid or Nodemailer in production
};
const updateInventory = (product) => {
// Simulate inventory update
console.log(`Updating inventory for product: ${product.name} (ID: ${product.id})`);
// Integrate with an inventory service or database in production
};
connectRabbitMQ();
module.exports = {};
Explanation:
• listen to product_events queue: Handles product_created events by simulating inventory updates.
• handleEvent: Processes both user_registered and product_created events.
________________________________________
5. Complete Code and Configuration Files
5.1. docker-compose.yml
yaml
Copy code
# docker-compose.yml
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- '5672:5672' # RabbitMQ server
- '15672:15672' # RabbitMQ Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
networks:
- backend
auth-service:
build:
context: ./auth-service
ports:
- '5003:5003'
environment:
- PORT=5003
- NODE_ENV=development
- JWT_SECRET=your_jwt_secret_key
- JWT_EXPIRES_IN=1h
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- rabbitmq
networks:
- backend
user-service:
build:
context: ./user-service
ports:
- '5001:5001'
environment:
- PORT=5001
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- auth-service
- rabbitmq
networks:
- backend
product-service:
build:
context: ./product-service
ports:
- '5002:5002'
environment:
- PORT=5002
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- auth-service
- rabbitmq
networks:
- backend
notification-service:
build:
context: ./notification-service
ports:
- '5004:5004'
environment:
- PORT=5004
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
restart: on-failure
depends_on:
- rabbitmq
networks:
- backend
api-gateway:
build:
context: ./api-gateway
ports:
- '8080:8080'
depends_on:
- user-service
- product-service
- auth-service
- rabbitmq
restart: on-failure
networks:
- backend
networks:
backend:
driver: bridge
Note: Ensure that the JWT_SECRET in auth-service matches the one specified in api-gateway/config/gateway.config.yml.
5.2. auth-service Code
(Already provided in Stage 3. No changes except adding event publishing in register method.)
5.3. product-service Code
(Already provided in Stage 2 with modifications for event publishing.)
5.4. notification-service Code
(Already provided in this stage.)
5.5. api-gateway Code
api-gateway/package.json
json
Copy code
{
"name": "api-gateway",
"version": "1.0.0",
"description": "API Gateway for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "express-gateway start"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express-gateway": "^2.30.0",
"jsonwebtoken": "^9.0.0",
"amqplib": "^0.10.3"
}
}
api-gateway/policies/jwt-auth/jwt-auth.js
(Already provided in Stage 3. No changes needed.)
api-gateway/config/gateway.config.yml
yaml
Copy code
# api-gateway/config/gateway.config.yml
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- jwt-auth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- jwt-auth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
Important:
• Replace 'your_jwt_secret_key' with the actual JWT_SECRET from auth-service/.env.
api-gateway/Dockerfile
dockerfile
Copy code
# api-gateway/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install Express Gateway globally
RUN npm install -g express-gateway
# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install
# Copy gateway configuration and policies
COPY . .
EXPOSE 8080
CMD [ "express-gateway", "start" ]
________________________________________
6. Running the Project
6.1. Start All Services
Ensure you're in the root directory ibrandidit-microservices/, then build and start all services using Docker Compose.
bash
Copy code
docker-compose up --build
Expected Output:
bash
Copy code
Creating network "ibrandidit-microservices_backend" with the default driver
Creating ibrandidit-microservices_rabbitmq_1 ... done
Creating ibrandidit-microservices_auth-service_1 ... done
Creating ibrandidit-microservices_user-service_1 ... done
Creating ibrandidit-microservices_product-service_1 ... done
Creating ibrandidit-microservices_notification-service_1 ... done
Creating ibrandidit-microservices_api-gateway_1 ... done
6.2. Access RabbitMQ Management UI
Open your browser and navigate to http://localhost:15672.
• Username: guest
• Password: guest
You can monitor queues, exchanges, and message flow here.
6.3. Access Service API Documentation
• Authentication Service: http://localhost:5003/api-docs
• User Service: http://localhost:5001/api-docs
• Product Service: http://localhost:5002/api-docs
• Notification Service: http://localhost:5004/api-docs
6.4. Testing the Event-Driven Flow
6.4.1. Register a New User
http
Copy code
POST http://localhost:5003/auth/register
Content-Type: application/json
{
"name": "Frank",
"email": "frank@example.com",
"password": "frankspassword"
}
Expected Response:
json
Copy code
{
"message": "User registered successfully",
"user": {
"id": 1697048168405,
"name": "Frank",
"email": "frank@example.com"
}
}
Expected Notification Service Output:
css
Copy code
Sending welcome email to frank@example.com
6.4.2. Create a New Product
http
Copy code
POST http://localhost:5002/products
Content-Type: application/json
{
"name": "Sneakers",
"price": 49.99
}
Expected Response:
json
Copy code
{
"id": 1697048168406,
"name": "Sneakers",
"price": 49.99
}
Expected Notification Service Output:
less
Copy code
Updating inventory for product: Sneakers (ID: 1697048168406)
________________________________________
7. Documentation and Usage
7.1. Accessing RabbitMQ Management UI
• URL: http://localhost:15672
• Purpose: Monitor queues, exchanges, and message flow.
• Usage:
o Queues: Check user_events and product_events queues to see messages.
o Exchanges: Verify how messages are routed.
o Messages: View or manually publish messages for testing.
7.2. API Workflow
1. User Registration:
o Action: User registers via the auth-service.
o Event Published: user_registered to user_events queue.
o Notification Service: Listens to user_events and sends a welcome email.
2. Product Creation:
o Action: New product added via the product-service.
o Event Published: product_created to product_events queue.
o Notification Service: Listens to product_events and updates inventory.
7.3. Monitoring Services
Use Docker logs or the RabbitMQ Management UI to monitor service interactions and message flows.
bash
Copy code
# View logs for a specific service
docker-compose logs auth-service
docker-compose logs product-service
docker-compose logs notification-service
________________________________________
8. Testing Services
8.1. Running Tests
Each service has its own set of tests. To run tests for a specific service:
bash
Copy code
# Navigate to the service directory
cd auth-service
npm test
cd ../product-service
npm test
cd ../notification-service
npm test
# Similarly for other services
Example Output:
bash
Copy code
PASS auth-service/tests/auth.test.js
Authentication API Endpoints
✓ should register a new user (XXX ms)
✓ should not register a user with existing email (XXX ms)
✓ should login an existing user (XXX ms)
✓ should not login with incorrect password (XXX ms)
✓ should not login a non-existent user (XXX ms)
✓ should retrieve a list of users with valid token (XXX ms)
✓ should not retrieve users without token (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: X.XXX s
8.2. Manual Testing
1. User Registration and Event Publishing:
o Register a new user via the auth-service API.
o Verify that the user_registered event appears in RabbitMQ.
o Check the notification-service logs to confirm handling of the event.
2. Product Creation and Event Publishing:
o Create a new product via the product-service API.
o Verify that the product_created event appears in RabbitMQ.
o Check the notification-service logs to confirm handling of the event.
3. API Gateway Authentication:
o Attempt to access protected routes without a token to ensure they are secured.
o Access protected routes with a valid token to ensure successful authorization.
________________________________________
9. Troubleshooting Tips
1. RabbitMQ Not Starting:
o Check Logs:
bash
Copy code
docker-compose logs rabbitmq
o Ensure Ports Are Free: Ports 5672 and 15672 should not be in use by other applications.
2. Services Failing to Connect to RabbitMQ:
o Verify RABBITMQ_URL: Ensure it's correctly set to amqp://guest:guest@rabbitmq:5672 in each service's .env file.
o Network Issues: Ensure all services are connected to the backend network.
3. Events Not Being Published or Consumed:
o Check Queues in RabbitMQ Management UI: Ensure messages are being published to the correct queues.
o Service Logs: Inspect logs for auth-service, product-service, and notification-service for any errors related to RabbitMQ.
4. JWT Authentication Issues:
o Secret Key Mismatch: Ensure the JWT_SECRET in auth-service matches the one in api-gateway/config/gateway.config.yml.
o Token Expiry: Verify that tokens are not expired when making requests.
5. Swagger UI Not Loading:
o Verify Service is Running: Ensure the service is up and accessible.
o Check Service Logs: Look for errors related to Swagger setup.
6. Docker Compose Issues:
o Rebuild Containers:
bash
Copy code
docker-compose up --build
o Restart Services:
bash
Copy code
docker-compose restart
7. Testing Failures:
o Ensure Services Are Running: Some tests may fail if dependent services are not up.
o Check Environment Variables: Ensure all required variables are set correctly.
________________________________________
10. Next Steps
With Stage 4 completed, you've successfully implemented an event-driven communication system using RabbitMQ. This setup allows your microservices to communicate asynchronously, improving scalability and reliability.
Proceed to Stage 5: Database Integration and Persistence
In the next stage, we'll integrate databases with your microservices, enabling data persistence and more complex data operations.
________________________________________
11. Additional Resources
• RabbitMQ Documentation: https://www.rabbitmq.com/documentation.html
• amqplib GitHub Repository: https://github.com/amqp-node/amqplib
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• Swagger Documentation: https://swagger.io/docs/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• Docker Documentation: https://docs.docker.com/
• Docker Compose Documentation: https://docs.docker.com/compose/
________________________________________
Disclaimer: This guide provides a foundational setup for event-driven communication in a microservices architecture. Depending on project complexity and requirements, further optimizations and configurations may be necessary.
Stage 5: Database Integration and Persistence for your eCommerce platform, iBranDIDit.com. In this stage, we'll integrate databases into your microservices to enable data persistence. We'll use MongoDB as our database due to its flexibility and seamless integration with Node.js applications. Each microservice will connect to its respective database collection, ensuring data is stored and retrieved efficiently.
________________________________________
Stage 5: Database Integration and Persistence
________________________________________
1. Overview
Objective:
Implement data persistence in your microservices architecture by integrating MongoDB. This involves:
1. Adding MongoDB to the Docker Compose Setup: Ensure a centralized database service is available to all microservices.
2. Defining Data Models: Create schemas for Users and Products.
3. Updating Microservices to Use MongoDB: Modify services to interact with the database instead of in-memory storage.
4. Enhancing Notification Service (Optional): If needed, connect the Notification Service to the database.
5. Ensuring Secure Database Connections: Manage sensitive data using environment variables.
6. Testing Database Operations: Update tests to interact with the database.
Benefits:
• Data Persistence: Ensures that data remains intact across service restarts and deployments.
• Scalability: Facilitates handling larger datasets and more complex queries.
• Reliability: Provides a robust data storage solution with backup and recovery options.
• Maintainability: Simplifies data management across multiple services.
________________________________________
2. Technology Stack
• Database: MongoDB
• ORM/ODM: Mongoose (for MongoDB object modeling)
• Backend Framework: Node.js with Express.js
• Containerization: Docker
• Orchestration: Docker Compose
• Libraries:
o mongoose: MongoDB object modeling for Node.js
• Testing: Jest and Supertest
• Documentation: Swagger (integrated into existing services)
________________________________________
3. Project Structure
We'll extend the existing ibrandidit-microservices project by integrating MongoDB and updating microservices to use it for data persistence.
lua
Copy code
ibrandidit-microservices/
├── api-gateway/
│ ├── config/
│ │ └── gateway.config.yml
│ ├── policies/
│ │ └── jwt-auth/
│ │ └── jwt-auth.js
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
├── auth-service/
│ ├── controllers/
│ │ └── authController.js
│ ├── middleware/
│ │ └── authenticate.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── auth.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ ├── Dockerfile
│ └── README.md
├── user-service/
│ ├── controllers/
│ │ └── userController.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── user.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ │ └── productController.js
│ ├── models/
│ │ └── Product.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── product.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── notification-service/
│ ├── controllers/
│ │ └── notificationController.js
│ ├── routes/
│ │ └── notificationRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── notification.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
└── README.md
________________________________________
4. Implementation Steps
4.1. Adding MongoDB to Docker Compose
We'll add a MongoDB service to the docker-compose.yml file, making it accessible to all microservices.
4.1.1. Update docker-compose.yml
yaml
Copy code
# docker-compose.yml
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- '5672:5672' # RabbitMQ server
- '15672:15672' # RabbitMQ Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
networks:
- backend
mongodb:
image: mongo:6.0
restart: always
ports:
- '27017:27017'
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
- mongo-data:/data/db
networks:
- backend
auth-service:
build:
context: ./auth-service
ports:
- '5003:5003'
environment:
- PORT=5003
- NODE_ENV=development
- JWT_SECRET=your_jwt_secret_key
- JWT_EXPIRES_IN=1h
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
restart: on-failure
depends_on:
- rabbitmq
- mongodb
networks:
- backend
user-service:
build:
context: ./user-service
ports:
- '5001:5001'
environment:
- PORT=5001
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- MONGODB_URI=mongodb://root:example@mongodb:27017/userdb?authSource=admin
restart: on-failure
depends_on:
- auth-service
- rabbitmq
- mongodb
networks:
- backend
product-service:
build:
context: ./product-service
ports:
- '5002:5002'
environment:
- PORT=5002
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
restart: on-failure
depends_on:
- auth-service
- rabbitmq
- mongodb
networks:
- backend
notification-service:
build:
context: ./notification-service
ports:
- '5004:5004'
environment:
- PORT=5004
- NODE_ENV=development
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- MONGODB_URI=mongodb://root:example@mongodb:27017/notificationdb?authSource=admin
restart: on-failure
depends_on:
- rabbitmq
- mongodb
networks:
- backend
api-gateway:
build:
context: ./api-gateway
ports:
- '8080:8080'
environment:
- MONGODB_URI=mongodb://root:example@mongodb:27017/api_gateway_db?authSource=admin
depends_on:
- user-service
- product-service
- auth-service
- rabbitmq
- mongodb
restart: on-failure
networks:
- backend
volumes:
mongo-data:
networks:
backend:
driver: bridge
Explanation:
• mongodb:
o image: Uses the official MongoDB image.
o ports: Exposes MongoDB on port 27017.
o environment: Sets the root username and password.
o volumes: Persists data using Docker volumes.
o networks: Connects to the backend network for internal communication.
• Microservices (auth-service, user-service, product-service, notification-service, api-gateway):
o environment:
MONGODB_URI: Connection string to the MongoDB service. Each service uses its own database (authdb, userdb, productdb, notificationdb, api_gateway_db).
o depends_on: Ensures services start after MongoDB and RabbitMQ.
o networks: Connects to the backend network.
Note: Replace your_jwt_secret_key with your actual JWT secret in auth-service.
4.1.2. Restart Docker Compose
After updating docker-compose.yml, rebuild and restart the services to include MongoDB.
bash
Copy code
cd ibrandidit-microservices/
docker-compose up --build
Expected Output:
bash
Copy code
Creating network "ibrandidit-microservices_backend" with the default driver
Creating ibrandidit-microservices_rabbitmq_1 ... done
Creating ibrandidit-microservices_mongodb_1 ... done
Creating ibrandidit-microservices_auth-service_1 ... done
Creating ibrandidit-microservices_user-service_1 ... done
Creating ibrandidit-microservices_product-service_1 ... done
Creating ibrandidit-microservices_notification-service_1 ... done
Creating ibrandidit-microservices_api-gateway_1 ... done
________________________________________
4.2. Defining Data Models
We'll define Mongoose schemas for Users and Products, enabling structured data storage.
4.2.1. Define User Model in auth-service
a. Create auth-service/models/User.js
javascript
Copy code
// auth-service/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
},
password: {
type: String,
required: true,
},
}, { timestamps: true });
module.exports = mongoose.model('User', UserSchema);
Explanation:
• Fields:
o name: User's full name.
o email: User's email, unique and indexed.
o password: Hashed password.
• Options:
o timestamps: Automatically adds createdAt and updatedAt fields.
4.2.2. Define User Model in user-service
If the User Service manages additional user data, define its own schema.
b. Create user-service/models/User.js
javascript
Copy code
// user-service/models/User.js
const mongoose = require('mongoose');
const UserProfileSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'auth-service.User',
required: true,
unique: true,
},
address: {
type: String,
trim: true,
},
phone: {
type: String,
trim: true,
},
// Add more profile-related fields as needed
}, { timestamps: true });
module.exports = mongoose.model('UserProfile', UserProfileSchema);
Explanation:
• Fields:
o userId: References the User model from the Auth Service.
o address: User's address.
o phone: User's phone number.
• Options:
o timestamps: Automatically adds createdAt and updatedAt fields.
4.2.3. Define Product Model in product-service
c. Create product-service/models/Product.js
javascript
Copy code
// product-service/models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
},
price: {
type: Number,
required: true,
},
description: {
type: String,
trim: true,
},
inventory: {
type: Number,
default: 0,
},
// Add more product-related fields as needed
}, { timestamps: true });
module.exports = mongoose.model('Product', ProductSchema);
Explanation:
• Fields:
o name: Product name.
o price: Product price.
o description: Product description.
o inventory: Number of items in stock.
• Options:
o timestamps: Automatically adds createdAt and updatedAt fields.
________________________________________
4.3. Updating Microservices to Use MongoDB
We'll modify each microservice to connect to MongoDB and use Mongoose for data operations.
4.3.1. Update auth-service to Use MongoDB
a. Install Mongoose
bash
Copy code
cd auth-service
npm install mongoose
b. Update auth-service/index.js
javascript
Copy code
// auth-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes'); // To manage users
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Auth Service connected to MongoDB'))
.catch((err) => console.error('Auth Service failed to connect to MongoDB:', err));
// Routes
app.use('/auth', authRoutes);
app.use('/users', userRoutes); // Protected route
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Authentication Service API');
});
// Start Server
const PORT = process.env.PORT || 5003;
app.listen(PORT, () => {
console.log(`Authentication Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
Explanation:
• Mongoose Connection:
o Connects to MongoDB using the MONGODB_URI environment variable.
o Handles successful and failed connections with appropriate logs.
c. Update auth-service/controllers/authController.js
Modify the controller to interact with the MongoDB database.
javascript
Copy code
// auth-service/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rabbitmqService = require('../services/rabbitmqService');
const User = require('../models/User');
/**
* @swagger
* components:
* schemas:
* RegisterInput:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* description: The name of the user
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* name: Alice
* email: alice@example.com
* password: securepassword
* LoginInput:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* description: The email of the user
* password:
* type: string
* description: The password of the user
* example:
* email: alice@example.com
* password: securepassword
*/
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
try {
// Check if user already exists
const existingUser = await User.findOne({ email: email.toLowerCase() });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = new User({
name,
email: email.toLowerCase(),
password: hashedPassword,
});
await newUser.save();
// Publish event to RabbitMQ
const event = {
event: 'user_registered',
data: {
id: newUser._id,
name: newUser.name,
email: newUser.email,
},
};
await rabbitmqService.publishEvent('user_events', event);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser._id, name, email: newUser.email } });
} catch (error) {
console.error('Error during registration:', error);
res.status(500).json({ message: 'Server error during registration' });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
// Basic validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
try {
// Find user
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Compare passwords
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
});
res.status(200).json({ message: 'Login successful', token });
} catch (error) {
console.error('Error during login:', error);
res.status(500).json({ message: 'Server error during login' });
}
};
Explanation:
• Registration (register):
o Validation: Ensures name, email, and password are provided.
o User Existence Check: Queries MongoDB to check if the user already exists.
o Password Hashing: Uses bcrypt to hash the password.
o User Creation: Saves the new user to MongoDB.
o Event Publishing: Publishes a user_registered event to RabbitMQ.
o Response: Returns a success message with user details (excluding password).
• Login (login):
o Validation: Ensures email and password are provided.
o User Retrieval: Fetches the user from MongoDB.
o Password Verification: Compares provided password with the hashed password in the database.
o JWT Issuance: Creates a JWT containing user ID and email.
o Response: Returns a success message with the JWT.
c. Update auth-service/routes/authRoutes.js
Ensure the routes point to the updated controller.
javascript
Copy code
// auth-service/routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
/**
* @swagger
* tags:
* name: Authentication
* description: User authentication and registration
*/
/**
* @swagger
* /auth/register:
* post:
* summary: Register a new user
* tags: [Authentication]
* requestBody:
* description: User object that needs to be registered
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RegisterInput'
* responses:
* 201:
* description: User registered successfully
* 400:
* description: Invalid input or user already exists
*/
router.post('/register', authController.register);
/**
* @swagger
* /auth/login:
* post:
* summary: Login a user
* tags: [Authentication]
* requestBody:
* description: User credentials for login
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LoginInput'
* responses:
* 200:
* description: Login successful, returns JWT
* 400:
* description: Invalid credentials
*/
router.post('/login', authController.login);
module.exports = router;
d. Update auth-service/tests/auth.test.js
Modify tests to interact with the MongoDB database.
javascript
Copy code
// auth-service/tests/auth.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const User = require('../models/User');
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await User.deleteMany({});
await mongoose.connection.close();
});
describe('Authentication API Endpoints', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'User registered successfully');
expect(res.body.user).toHaveProperty('id');
expect(res.body.user).toHaveProperty('name', 'Charlie');
expect(res.body.user).toHaveProperty('email', 'charlie@example.com');
});
it('should not register a user with existing email', async () => {
// Register the same user again
const res = await request(app)
.post('/auth/register')
.send({
name: 'Charlie',
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User already exists');
});
it('should login an existing user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Login successful');
expect(res.body).toHaveProperty('token');
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'wrongpassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should not login a non-existent user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should retrieve a list of users with valid token', async () => {
// First, login to get the token
const loginRes = await request(app)
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'securepassword',
});
const token = loginRes.body.token;
// Access protected route
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Charlie');
});
it('should not retrieve users without token', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Access token missing');
});
});
Explanation:
• Database Connection:
o beforeAll: Connects to MongoDB before running tests.
o afterAll: Cleans up by deleting test users and closing the connection.
• Tests:
o Registration Tests: Register a new user and ensure duplicate registrations are handled.
o Login Tests: Test successful login and invalid login scenarios.
o Protected Route Test: Access the /users route with and without a valid token.
4.3.2. Update user-service to Use MongoDB
a. Install Mongoose
bash
Copy code
cd user-service
npm install mongoose
b. Update user-service/index.js
javascript
Copy code
// user-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const userRoutes = require('./routes/userRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('User Service connected to MongoDB'))
.catch((err) => console.error('User Service failed to connect to MongoDB:', err));
// Routes
app.use('/users', userRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to User Service API');
});
// Start Server
const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
console.log(`User Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
c. Update user-service/models/User.js
Define the User Profile schema.
javascript
Copy code
// user-service/models/User.js
const mongoose = require('mongoose');
const UserProfileSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
unique: true,
},
address: {
type: String,
trim: true,
},
phone: {
type: String,
trim: true,
},
// Add more profile-related fields as needed
}, { timestamps: true });
module.exports = mongoose.model('UserProfile', UserProfileSchema);
d. Update user-service/controllers/userController.js
Modify the controller to interact with MongoDB.
javascript
Copy code
// user-service/controllers/userController.js
const UserProfile = require('../models/User');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
exports.getUsers = async (req, res) => {
try {
const profiles = await UserProfile.find().populate('userId', 'name email');
res.status(200).json(profiles);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ message: 'Server error fetching users' });
}
};
exports.getUserById = async (req, res) => {
const userId = req.params.id;
try {
const profile = await UserProfile.findOne({ userId }).populate('userId', 'name email');
if (profile) {
res.status(200).json(profile);
} else {
res.status(404).json({ message: 'User profile not found' });
}
} catch (error) {
console.error('Error fetching user by ID:', error);
res.status(500).json({ message: 'Server error fetching user' });
}
};
exports.createUser = async (req, res) => {
const { userId, address, phone } = req.body;
if (!userId) {
return res.status(400).json({ message: 'User ID is required' });
}
try {
// Check if profile already exists
const existingProfile = await UserProfile.findOne({ userId });
if (existingProfile) {
return res.status(400).json({ message: 'User profile already exists' });
}
const newProfile = new UserProfile({
userId,
address,
phone,
});
await newProfile.save();
res.status(201).json(newProfile);
} catch (error) {
console.error('Error creating user profile:', error);
res.status(500).json({ message: 'Server error creating user profile' });
}
};
Explanation:
• getUsers: Retrieves all user profiles and populates user details from the Auth Service.
• getUserById: Retrieves a specific user profile based on userId.
• createUser: Creates a new user profile linked to the userId.
e. Update user-service/routes/userRoutes.js
Ensure the routes point to the updated controller.
javascript
Copy code
// user-service/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authenticate = require('../middleware/authenticate');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of user profiles (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of user profiles.
* 401:
* description: Unauthorized
*/
router.get('/', authenticate, userController.getUsers);
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user profile by User ID (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* description: User ID of the profile to retrieve
* schema:
* type: string
* responses:
* 200:
* description: User profile found
* 401:
* description: Unauthorized
* 404:
* description: User profile not found
*/
router.get('/:id', authenticate, userController.getUserById);
/**
* @swagger
* /users:
* post:
* summary: Create a new user profile (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* description: User profile object that needs to be created
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserProfileInput'
* responses:
* 201:
* description: User profile created successfully
* 400:
* description: Invalid input or profile already exists
* 401:
* description: Unauthorized
*/
router.post('/', authenticate, userController.createUser);
module.exports = router;
f. Create user-service/middleware/authenticate.js
If not already present, ensure authentication middleware is available.
javascript
Copy code
// user-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
module.exports = function (req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
// Add user info to headers for downstream services
req.headers['x-user-id'] = user.id;
req.headers['x-user-email'] = user.email;
next();
});
};
Explanation:
• JWT Verification: Validates the JWT and attaches user information to the request.
• Error Handling: Returns appropriate error messages for missing or invalid tokens.
g. Update user-service/tests/user.test.js
Modify tests to interact with MongoDB.
javascript
Copy code
// user-service/tests/user.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const UserProfile = require('../models/User');
let token;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service
// Assuming auth-service is running and accessible
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Test User',
email: 'testuser@example.com',
password: 'testpassword',
});
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'testuser@example.com',
password: 'testpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await UserProfile.deleteMany({});
await mongoose.connection.close();
});
describe('User Service API Endpoints', () => {
it('should create a new user profile', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: '60c72b2f9b1e8e1d4c8e4f8c', // Replace with actual userId from auth-service
address: '123 Test Street',
phone: '123-456-7890',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', '60c72b2f9b1e8e1d4c8e4f8c');
expect(res.body).toHaveProperty('address', '123 Test Street');
expect(res.body).toHaveProperty('phone', '123-456-7890');
});
it('should not create a user profile with existing userId', async () => {
// Attempt to create the same profile again
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: '60c72b2f9b1e8e1d4c8e4f8c', // Same userId
address: '123 Test Street',
phone: '123-456-7890',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User profile already exists');
});
it('should retrieve a list of user profiles', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('userId');
expect(res.body[0].userId).toHaveProperty('name');
expect(res.body[0].userId).toHaveProperty('email');
});
it('should retrieve a user profile by user ID', async () => {
// Fetch the user profile created earlier
const profiles = await UserProfile.find().limit(1);
const userId = profiles[0].userId.toString();
const res = await request(app)
.get(`/users/${userId}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('userId');
expect(res.body.userId).toHaveProperty('name');
expect(res.body.userId).toHaveProperty('email');
});
it('should not retrieve a non-existent user profile', async () => {
const res = await request(app)
.get('/users/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'User profile not found');
});
});
Explanation:
• beforeAll:
o Registers and logs in a test user via the Auth Service to obtain a valid JWT.
o Note: Replace '60c72b2f9b1e8e1d4c8e4f8c' with the actual userId generated during registration.
• Tests:
o Create User Profile: Ensures a new profile can be created.
o Prevent Duplicate Profiles: Ensures duplicate profiles are not allowed.
o Retrieve All Profiles: Checks if all profiles can be retrieved.
o Retrieve Specific Profile: Fetches a profile by userId.
o Handle Non-existent Profiles: Ensures appropriate error handling.
h. Update user-service/Dockerfile
Ensure the Dockerfile includes the necessary dependencies.
dockerfile
Copy code
# user-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5001
CMD [ "npm", "start" ]
4.3.3. Update product-service to Use MongoDB
a. Install Mongoose
bash
Copy code
cd product-service
npm install mongoose
b. Update product-service/index.js
javascript
Copy code
// product-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const productRoutes = require('./routes/productRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Product Service connected to MongoDB'))
.catch((err) => console.error('Product Service failed to connect to MongoDB:', err));
// Routes
app.use('/products', productRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Product Service API');
});
// Start Server
const PORT = process.env.PORT || 5002;
app.listen(PORT, () => {
console.log(`Product Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
c. Update product-service/controllers/productController.js
Modify the controller to interact with MongoDB.
javascript
Copy code
// product-service/controllers/productController.js
const Product = require('../models/Product');
const rabbitmqService = require('../services/rabbitmqService');
/**
* @swagger
* tags:
* name: Products
* description: Product management
*/
exports.getProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json(products);
} catch (error) {
console.error('Error fetching products:', error);
res.status(500).json({ message: 'Server error fetching products' });
}
};
exports.getProductById = async (req, res) => {
const productId = req.params.id;
try {
const product = await Product.findById(productId);
if (product) {
res.status(200).json(product);
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
console.error('Error fetching product by ID:', error);
res.status(500).json({ message: 'Server error fetching product' });
}
};
exports.createProduct = async (req, res) => {
const { name, price, description, inventory } = req.body;
if (!name || price === undefined) {
return res.status(400).json({ message: 'Name and price are required' });
}
try {
const newProduct = new Product({
name,
price,
description,
inventory: inventory || 0,
});
await newProduct.save();
// Publish event to RabbitMQ
const event = {
event: 'product_created',
data: {
id: newProduct._id,
name: newProduct.name,
price: newProduct.price,
},
};
await rabbitmqService.publishEvent('product_events', event);
res.status(201).json(newProduct);
} catch (error) {
console.error('Error creating product:', error);
res.status(500).json({ message: 'Server error creating product' });
}
};
Explanation:
• getProducts: Retrieves all products from MongoDB.
• getProductById: Retrieves a specific product by its ID.
• createProduct: Creates a new product and publishes a product_created event to RabbitMQ.
d. Update product-service/routes/productRoutes.js
Ensure the routes point to the updated controller.
javascript
Copy code
// product-service/routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
const authenticate = require('../middleware/authenticate'); // If product routes are protected
/**
* @swagger
* tags:
* name: Products
* description: Product management
*/
/**
* @swagger
* /products:
* get:
* summary: Retrieve a list of products
* tags: [Products]
* responses:
* 200:
* description: A list of products.
* 500:
* description: Server error
*/
router.get('/', productController.getProducts);
/**
* @swagger
* /products/{id}:
* get:
* summary: Get a product by ID
* tags: [Products]
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the product to retrieve
* schema:
* type: string
* responses:
* 200:
* description: Product found
* 404:
* description: Product not found
* 500:
* description: Server error
*/
router.get('/:id', productController.getProductById);
/**
* @swagger
* /products:
* post:
* summary: Create a new product
* tags: [Products]
* requestBody:
* description: Product object that needs to be added
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ProductInput'
* responses:
* 201:
* description: Product created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', productController.createProduct);
module.exports = router;
Note: If product routes need to be protected, uncomment the authenticate middleware and adjust accordingly.
e. Update product-service/tests/product.test.js
Modify tests to interact with MongoDB.
javascript
Copy code
// product-service/tests/product.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const Product = require('../models/Product');
let token;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Product Tester',
email: 'producttester@example.com',
password: 'productpassword',
});
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'producttester@example.com',
password: 'productpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await Product.deleteMany({});
await mongoose.connection.close();
});
describe('Product Service API Endpoints', () => {
it('should create a new product', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Test Product',
price: 99.99,
description: 'A product for testing purposes',
inventory: 100,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
expect(res.body).toHaveProperty('description', 'A product for testing purposes');
expect(res.body).toHaveProperty('inventory', 100);
});
it('should not create a product without name and price', async () => {
const res = await request(app)
.post('/products')
.send({
description: 'Missing name and price',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Name and price are required');
});
it('should retrieve a list of products', async () => {
const res = await request(app)
.get('/products');
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Test Product');
});
it('should retrieve a product by ID', async () => {
// Fetch the product created earlier
const products = await Product.find().limit(1);
const productId = products[0]._id.toString();
const res = await request(app)
.get(`/products/${productId}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
});
it('should not retrieve a non-existent product', async () => {
const res = await request(app)
.get('/products/60c72b2f9b1e8e1d4c8e4f8d'); // Assuming this ID doesn't exist
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Product not found');
});
});
Explanation:
• beforeAll:
o Registers and logs in a test user via the Auth Service to obtain a valid JWT.
• Tests:
o Create Product: Ensures a new product can be created.
o Prevent Invalid Products: Ensures products cannot be created without mandatory fields.
o Retrieve All Products: Checks if all products can be retrieved.
o Retrieve Specific Product: Fetches a product by its ID.
o Handle Non-existent Products: Ensures appropriate error handling.
f. Update product-service/Dockerfile
Ensure the Dockerfile includes the necessary dependencies.
dockerfile
Copy code
# product-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5002
CMD [ "npm", "start" ]
4.4. Updating the Notification Service (Optional)
If the Notification Service needs to store notification records or interact with the database, integrate it accordingly. For simplicity, we'll assume it only logs events.
No changes are required unless specific data persistence is needed.
________________________________________
4.5. Enhancing the API Gateway (Optional)
If the API Gateway needs to interact with the database for specific purposes (e.g., rate limiting based on user data), integrate MongoDB similarly. For this stage, no changes are necessary unless additional features are planned.
________________________________________
5. Complete Code and Configuration Files
5.1. docker-compose.yml
(Already provided above in section 4.1.1)
5.2. Authentication Service (auth-service)
auth-service/package.json
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
auth-service/.env
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
Note: Replace your_jwt_secret_key with a secure secret.
auth-service/models/User.js
(Already provided above)
auth-service/index.js
(Already provided above)
auth-service/controllers/authController.js
(Already provided above)
auth-service/routes/authRoutes.js
(Already provided above)
auth-service/middleware/authenticate.js
(Already provided in Stage 3. If not present, create it accordingly.)
auth-service/services/rabbitmqService.js
(Already provided in Stage 4)
auth-service/tests/auth.test.js
(Already provided above)
auth-service/swagger.js
(Already provided above)
auth-service/Dockerfile
(Already provided in Stage 4)
________________________________________
5.3. User Service (user-service)
user-service/package.json
json
Copy code
{
"name": "user-service",
"version": "1.0.0",
"description": "User Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
user-service/.env
env
Copy code
PORT=5001
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/userdb?authSource=admin
user-service/models/User.js
(Already provided above)
user-service/index.js
(Already provided above)
user-service/controllers/userController.js
(Already provided above)
user-service/routes/userRoutes.js
(Already provided above)
user-service/middleware/authenticate.js
(Already provided in Stage 3. If not present, create it accordingly.)
user-service/services/rabbitmqService.js
If the User Service needs to publish events, implement similar to auth-service. For now, assuming it only consumes, no changes required.
user-service/tests/user.test.js
(Already provided above)
user-service/swagger.js
(Already provided in Stage 3 or earlier stages. Ensure it includes User Profile endpoints.)
user-service/Dockerfile
(Already provided above)
________________________________________
5.4. Product Service (product-service)
product-service/package.json
json
Copy code
{
"name": "product-service",
"version": "1.0.0",
"description": "Product Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
product-service/.env
env
Copy code
PORT=5002
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
product-service/models/Product.js
(Already provided above)
product-service/index.js
(Already provided above)
product-service/controllers/productController.js
(Already provided above)
product-service/routes/productRoutes.js
(Already provided above)
product-service/middleware/authenticate.js
If product routes are to be protected, implement similar to other services. Otherwise, no changes needed.
product-service/services/rabbitmqService.js
(Already provided in Stage 4)
product-service/tests/product.test.js
(Already provided above)
product-service/swagger.js
(Already provided above)
product-service/Dockerfile
(Already provided above)
________________________________________
5.5. Notification Service (notification-service)
Since the Notification Service is primarily a consumer of events and not storing data, we might not need to connect it to MongoDB. However, if you plan to store notifications or logs, integrate MongoDB accordingly.
notification-service/package.json
json
Copy code
{
"name": "notification-service",
"version": "1.0.0",
"description": "Notification Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
notification-service/.env
env
Copy code
PORT=5004
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/notificationdb?authSource=admin
Note: If storing notifications, implement models accordingly.
notification-service/index.js
(Already provided in Stage 4)
notification-service/controllers/notificationController.js
(Optional: Implement if you plan to expose APIs for notifications.)
notification-service/routes/notificationRoutes.js
(Optional: Implement if you plan to expose APIs for notifications.)
notification-service/services/rabbitmqService.js
(Already provided in Stage 4)
notification-service/tests/notification.test.js
(Already provided in Stage 4)
notification-service/swagger.js
(Already provided in Stage 4)
notification-service/Dockerfile
(Already provided in Stage 4)
________________________________________
5.6. API Gateway (api-gateway)
No changes required unless you plan to interact with the database directly.
api-gateway/package.json
(Already provided in Stage 4)
api-gateway/config/gateway.config.yml
(Already provided in Stage 4. Ensure JWT_SECRET matches auth-service.)
api-gateway/policies/jwt-auth/jwt-auth.js
(Already provided in Stage 4)
api-gateway/Dockerfile
(Already provided in Stage 4)
________________________________________
6. Testing the Database Integration
6.1. Testing Authentication Service
1. Run Tests
bash
Copy code
cd auth-service
npm test
Expected Output:
sql
Copy code
PASS auth-service/tests/auth.test.js
Authentication API Endpoints
✓ should register a new user (XXX ms)
✓ should not register a user with existing email (XXX ms)
✓ should login an existing user (XXX ms)
✓ should not login with incorrect password (XXX ms)
✓ should not login a non-existent user (XXX ms)
✓ should retrieve a list of users with valid token (XXX ms)
✓ should not retrieve users without token (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: X.XXX s
2. Manual Testing
o Register a New User:
http
Copy code
POST http://localhost:5003/auth/register
Content-Type: application/json
{
"name": "Grace",
"email": "grace@example.com",
"password": "gracespassword"
}
Expected Response:
json
Copy code
{
"message": "User registered successfully",
"user": {
"id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Grace",
"email": "grace@example.com"
}
}
o Login the User:
http
Copy code
POST http://localhost:5003/auth/login
Content-Type: application/json
{
"email": "grace@example.com",
"password": "gracespassword"
}
Expected Response:
json
Copy code
{
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}
o Access Protected Route:
http
Copy code
GET http://localhost:5003/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Expected Response:
json
Copy code
[
{
"id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Grace",
"email": "grace@example.com"
}
]
6.2. Testing User Service
1. Run Tests
bash
Copy code
cd user-service
npm test
Expected Output:
sql
Copy code
PASS user-service/tests/user.test.js
User Service API Endpoints
✓ should create a new user profile (XXX ms)
✓ should not create a user profile with existing userId (XXX ms)
✓ should retrieve a list of user profiles (XXX ms)
✓ should retrieve a user profile by user ID (XXX ms)
✓ should not retrieve a non-existent user profile (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: X.XXX s
2. Manual Testing
o Create a User Profile:
http
Copy code
POST http://localhost:5001/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Content-Type: application/json
{
"userId": "60d5f9f9b1e8e1d4c8e4f8c5",
"address": "456 Example Avenue",
"phone": "987-654-3210"
}
Expected Response:
json
Copy code
{
"_id": "60d5fa0ab1e8e1d4c8e4f8c6",
"userId": "60d5f9f9b1e8e1d4c8e4f8c5",
"address": "456 Example Avenue",
"phone": "987-654-3210",
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
o Retrieve All User Profiles:
http
Copy code
GET http://localhost:5001/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Expected Response:
json
Copy code
[
{
"_id": "60d5fa0ab1e8e1d4c8e4f8c6",
"userId": {
"_id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Grace",
"email": "grace@example.com"
},
"address": "456 Example Avenue",
"phone": "987-654-3210",
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
]
o Retrieve a Specific User Profile:
http
Copy code
GET http://localhost:5001/users/60d5f9f9b1e8e1d4c8e4f8c5
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Expected Response:
json
Copy code
{
"_id": "60d5fa0ab1e8e1d4c8e4f8c6",
"userId": {
"_id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Grace",
"email": "grace@example.com"
},
"address": "456 Example Avenue",
"phone": "987-654-3210",
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
6.3. Testing Product Service
1. Run Tests
bash
Copy code
cd product-service
npm test
Expected Output:
less
Copy code
PASS product-service/tests/product.test.js
Product Service API Endpoints
✓ should create a new product (XXX ms)
✓ should not create a product without name and price (XXX ms)
✓ should retrieve a list of products (XXX ms)
✓ should retrieve a product by ID (XXX ms)
✓ should not retrieve a non-existent product (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: X.XXX s
2. Manual Testing
o Create a New Product:
http
Copy code
POST http://localhost:5002/products
Content-Type: application/json
{
"name": "Test Product",
"price": 99.99,
"description": "A product for testing purposes",
"inventory": 100
}
Expected Response:
json
Copy code
{
"_id": "60d5fb0ab1e8e1d4c8e4f8c7",
"name": "Test Product",
"price": 99.99,
"description": "A product for testing purposes",
"inventory": 100,
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
o Retrieve All Products:
http
Copy code
GET http://localhost:5002/products
Expected Response:
json
Copy code
[
{
"_id": "60d5fb0ab1e8e1d4c8e4f8c7",
"name": "Test Product",
"price": 99.99,
"description": "A product for testing purposes",
"inventory": 100,
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
]
o Retrieve a Specific Product:
http
Copy code
GET http://localhost:5002/products/60d5fb0ab1e8e1d4c8e4f8c7
Expected Response:
json
Copy code
{
"_id": "60d5fb0ab1e8e1d4c8e4f8c7",
"name": "Test Product",
"price": 99.99,
"description": "A product for testing purposes",
"inventory": 100,
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
6.4. Testing Notification Service
1. Run Tests
bash
Copy code
cd notification-service
npm test
Expected Output:
vbnet
Copy code
PASS notification-service/tests/notification.test.js
Notification Service API Endpoints
✓ should return welcome message (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: X.XXX s
2. Manual Testing
o Trigger Events:
User Registered: Register a new user via the Auth Service and observe the Notification Service logs for the welcome email simulation.
Product Created: Create a new product via the Product Service and observe the Notification Service logs for the inventory update simulation.
________________________________________
7. Documentation and Usage
7.1. Accessing MongoDB
• MongoDB Management UI:
o Option 1: Use MongoDB Compass (GUI tool) to connect to your local MongoDB instance.
Connection String: mongodb://root:example@localhost:27017
o Option 2: Use Mongo Shell.
Command:
bash
Copy code
mongo -u root -p example --authenticationDatabase admin
7.2. Accessing RabbitMQ Management UI
• URL: http://localhost:15672
• Username: guest
• Password: guest
• Usage:
o Queues: Monitor user_events and product_events queues.
o Exchanges: Observe message routing.
o Messages: View message details or manually publish messages for testing.
7.3. Accessing Service API Documentation
Each microservice has its own Swagger documentation:
• Authentication Service: http://localhost:5003/api-docs
• User Service: http://localhost:5001/api-docs
• Product Service: http://localhost:5002/api-docs
• Notification Service: http://localhost:5004/api-docs
Usage:
• Explore Endpoints: Use Swagger UI to interact with API endpoints.
• Understand Schemas: View request and response schemas for each endpoint.
7.4. Workflow Summary
1. User Registration:
o Action: User registers via the Auth Service (/auth/register).
o Event Published: user_registered to user_events queue.
o Notification Service: Listens to user_events and sends a welcome email.
2. User Login:
o Action: User logs in via the Auth Service (/auth/login).
o Response: Receives a JWT for authenticated requests.
3. User Profile Management:
o Action: Authenticated users can create and manage their profiles via the User Service (/users).
4. Product Management:
o Action: Create, retrieve, and manage products via the Product Service (/products).
o Event Published: product_created to product_events queue.
o Notification Service: Listens to product_events and updates inventory.
5. API Gateway:
o Action: Routes requests to respective services.
o Authentication: Validates JWTs for protected routes using the jwt-auth policy.
7.5. Deployment Considerations
• Environment Variables: Ensure all environment variables in .env files are correctly set, especially sensitive information like JWT_SECRET and MONGODB_URI.
• Volumes: Data in MongoDB is persisted using Docker volumes (mongo-data). Ensure volume management is handled appropriately.
• Scaling Services: Utilize Docker Compose's scaling features to handle increased load (e.g., multiple instances of user-service).
bash
Copy code
docker-compose up --scale user-service=3 -d
________________________________________
8. Troubleshooting Tips
1. MongoDB Connection Issues:
o Verify Connection String: Ensure MONGODB_URI is correctly set in each service's .env file.
o Check MongoDB Logs:
bash
Copy code
docker-compose logs mongodb
o Ensure MongoDB is Running: Check Docker Compose services are up and running.
2. RabbitMQ Connection Issues:
o Verify Connection String: Ensure RABBITMQ_URL is correctly set in each service's .env file.
o Check RabbitMQ Logs:
bash
Copy code
docker-compose logs rabbitmq
o Ensure RabbitMQ is Running: Check Docker Compose services are up and running.
3. Service Communication Issues:
o Check Docker Networks: Ensure all services are connected to the backend network.
o Ping Services: Use Docker commands to ping services internally.
4. JWT Authentication Issues:
o Secret Key Mismatch: Ensure JWT_SECRET is consistent across the Auth Service and API Gateway.
o Token Expiry: Ensure tokens are not expired when making requests.
5. Swagger UI Not Loading:
o Service Running: Ensure the respective service is up and accessible.
o Correct Paths: Verify that Swagger setup points to the correct route and controller files.
6. Testing Failures:
o Ensure Services are Running: Some tests may fail if dependent services are not up.
o Check Environment Variables: Ensure all required variables are set correctly.
o Database State: Clean the database before running tests to avoid conflicts.
7. Docker Compose Issues:
o Rebuild Containers:
bash
Copy code
docker-compose up --build
o Restart Services:
bash
Copy code
docker-compose restart
o Check Resource Allocation: Ensure Docker has enough resources allocated on your machine.
8. Event Handling Issues:
o Check Queues in RabbitMQ: Ensure messages are being published to the correct queues.
o Service Logs: Inspect logs for auth-service, product-service, and notification-service for any errors related to RabbitMQ.
________________________________________
9. Next Steps
With Stage 5 completed, you've successfully integrated MongoDB for data persistence in your microservices architecture. Your services can now store and retrieve data reliably, enhancing the overall robustness of your eCommerce platform.
Proceed to Stage 6: Implementing Advanced Features (e.g., Shopping Cart, Order Management)
In the next stage, we'll introduce advanced eCommerce features such as a Shopping Cart Service and Order Management Service, further expanding the microservices architecture.
________________________________________
10. Additional Resources
• MongoDB Documentation: https://docs.mongodb.com/
• Mongoose Documentation: https://mongoosejs.com/docs/
• RabbitMQ Documentation: https://www.rabbitmq.com/documentation.html
• amqplib GitHub Repository: https://github.com/amqp-node/amqplib
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• Swagger Documentation: https://swagger.io/docs/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• Docker Documentation: https://docs.docker.com/
• Docker Compose Documentation: https://docs.docker.com/compose/
• Docker Volume Management: https://docs.docker.com/storage/volumes/
• JWT Best Practices: https://auth0.com/docs/security/tokens/json-web-tokens/json-web-token-best-practices
________________________________________
Disclaimer: This guide provides a foundational setup for integrating MongoDB into a microservices architecture. Depending on project complexity and requirements, further optimizations and configurations may be necessary, such as implementing database indexing, optimizing queries, handling migrations, and ensuring data security.
________________________________________
By following this comprehensive guide for Stage 5: Database Integration and Persistence, you've successfully implemented robust data storage solutions within your microservices architecture. This setup ensures that your eCommerce platform can handle data efficiently, paving the way for more complex and scalable features in subsequent stages.
Stage 6: Consistent Data Models for your eCommerce platform, iBranDIDit.com. In this stage, we'll ensure that all microservices use consistent data models, which is crucial for maintaining data integrity and seamless communication between services. We'll achieve this by creating a shared common library that defines standardized data models and event schemas. This approach minimizes duplication and ensures that all services adhere to the same data structures.
________________________________________
Stage 6: Consistent Data Models
________________________________________
1. Overview
Objective:
• Standardize Data Models: Ensure all microservices use consistent data models for shared entities like Users and Products.
• Define Event Schemas: Standardize the structure of events published and consumed via RabbitMQ.
• Create a Shared Common Library: Develop a reusable library that houses shared data models and schemas, promoting DRY (Don't Repeat Yourself) principles.
• Integrate Common Library into Services: Update existing microservices to utilize the shared models, ensuring consistency across the architecture.
Benefits:
• Data Integrity: Consistent models prevent discrepancies and errors during data exchange between services.
• Ease of Maintenance: Centralized definitions simplify updates and modifications.
• Scalability: New services can easily adopt standardized models, facilitating seamless integration.
• Reduced Duplication: Avoids repetitive code across multiple services, enhancing codebase maintainability.
________________________________________
2. Technology Stack
• Language & Framework: Node.js with Express.js
• Shared Library Management: Local NPM package using NPM Workspaces
• Data Modeling: Mongoose (for MongoDB schemas)
• Event Schemas: JSON Schema for event validation
• Containerization: Docker
• Orchestration: Docker Compose
• Testing: Jest and Supertest
________________________________________
3. Project Structure
We'll introduce a common directory at the root of the project, structured as an NPM workspace. This workspace will house shared data models and event schemas. Each microservice will depend on the common library to utilize standardized models.
lua
Copy code
ibrandidit-microservices/
├── common/
│ ├── models/
│ │ ├── User.js
│ │ └── Product.js
│ ├── schemas/
│ │ ├── userRegistered.json
│ │ └── productCreated.json
│ ├── package.json
│ └── README.md
├── api-gateway/
│ ├── config/
│ │ └── gateway.config.yml
│ ├── policies/
│ │ └── jwt-auth/
│ │ └── jwt-auth.js
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
├── auth-service/
│ ├── controllers/
│ │ └── authController.js
│ ├── middleware/
│ │ └── authenticate.js
│ ├── models/
│ ├── routes/
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── auth.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ ├── Dockerfile
│ └── README.md
├── user-service/
│ ├── controllers/
│ │ └── userController.js
│ ├── models/
│ │ └── UserProfile.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── user.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ │ └── productController.js
│ ├── models/
│ │ └── Product.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── product.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── notification-service/
│ ├── controllers/
│ │ └── notificationController.js
│ ├── routes/
│ │ └── notificationRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── notification.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
├── package.json
└── README.md
Explanation:
• common/: Houses shared data models (models/) and event schemas (schemas/). This directory is managed as an NPM workspace, making it accessible to all microservices.
• Microservices (auth-service, user-service, product-service, notification-service, api-gateway): Each service will depend on the common library to utilize standardized models and schemas.
________________________________________
4. Implementation Steps
4.1. Setting Up NPM Workspaces
NPM Workspaces allow managing multiple packages within a single repository. We'll configure the root package.json to recognize common and all microservices as workspaces.
4.1.1. Update Root package.json
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service"
],
"scripts": {
"start": "docker-compose up --build",
"stop": "docker-compose down"
},
"dependencies": {}
}
Explanation:
• private: true: Prevents accidental publishing of the root package.
• workspaces: Lists all sub-packages, including common and each microservice.
4.1.2. Initialize NPM Workspaces
From the root directory (ibrandidit-microservices/), run:
bash
Copy code
npm install
This command will set up the workspaces, allowing microservices to reference the common package.
________________________________________
4.2. Creating the Common Library
We'll create a common library that includes standardized Mongoose models and JSON Schemas for events.
4.2.1. Initialize common Package
bash
Copy code
cd common
npm init -y
4.2.2. Install Dependencies in common
bash
Copy code
npm install mongoose jsonschema
• mongoose: For defining data models.
• jsonschema: For validating JSON events against schemas.
4.2.3. Define Shared Mongoose Models
a. Create common/models/User.js
javascript
Copy code
// common/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
},
password: {
type: String,
required: true,
},
}, { timestamps: true });
module.exports = mongoose.model('User', UserSchema);
b. Create common/models/Product.js
javascript
Copy code
// common/models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
},
price: {
type: Number,
required: true,
},
description: {
type: String,
trim: true,
},
inventory: {
type: Number,
default: 0,
},
}, { timestamps: true });
module.exports = mongoose.model('Product', ProductSchema);
4.2.4. Define Event Schemas
We'll use JSON Schema to define the structure of events published via RabbitMQ.
a. Create common/schemas/userRegistered.json
json
Copy code
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UserRegisteredEvent",
"type": "object",
"properties": {
"event": {
"type": "string",
"enum": ["user_registered"]
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User ID"
},
"name": {
"type": "string",
"description": "User's name"
},
"email": {
"type": "string",
"format": "email",
"description": "User's email"
}
},
"required": ["id", "name", "email"]
}
},
"required": ["event", "data"]
}
b. Create common/schemas/productCreated.json
json
Copy code
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProductCreatedEvent",
"type": "object",
"properties": {
"event": {
"type": "string",
"enum": ["product_created"]
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Product ID"
},
"name": {
"type": "string",
"description": "Product name"
},
"price": {
"type": "number",
"description": "Product price"
}
},
"required": ["id", "name", "price"]
}
},
"required": ["event", "data"]
}
4.2.5. Export Models and Schemas
c. Update common/package.json
Add a main entry and scripts if needed.
json
Copy code
{
"name": "common",
"version": "1.0.0",
"description": "Shared models and schemas for iBranDIDit.com microservices",
"main": "index.js",
"scripts": {
"test": "echo \"No tests for common library\""
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"jsonschema": "^1.4.0",
"mongoose": "^7.0.3"
}
}
d. Create common/index.js
Export the models and schemas for easy import in microservices.
javascript
Copy code
// common/index.js
const User = require('./models/User');
const Product = require('./models/Product');
const userRegisteredSchema = require('./schemas/userRegistered.json');
const productCreatedSchema = require('./schemas/productCreated.json');
module.exports = {
User,
Product,
schemas: {
userRegistered: userRegisteredSchema,
productCreated: productCreatedSchema,
},
};
4.2.6. Update .gitignore in common
Ensure unnecessary files are ignored.
gitignore
Copy code
# common/.gitignore
node_modules/
________________________________________
4.3. Integrating the Common Library into Microservices
Each microservice will install and utilize the common library to access standardized models and event schemas.
4.3.1. Installing common as a Workspace Dependency
Since we're using NPM Workspaces, installing common is straightforward.
For each microservice (auth-service, user-service, product-service, notification-service, api-gateway), perform the following steps:
a. Add common as a Dependency
Navigate to the microservice directory and add common as a dependency using the workspace protocol.
bash
Copy code
cd auth-service
npm install common@1.0.0
Note: NPM Workspaces automatically link local packages, so common will be accessible as a local dependency.
Repeat this step for each microservice:
bash
Copy code
cd ../user-service
npm install common@1.0.0
cd ../product-service
npm install common@1.0.0
cd ../notification-service
npm install common@1.0.0
cd ../api-gateway
npm install common@1.0.0
b. Verify Installation
Ensure that the common package is correctly installed by checking the node_modules directory and the package-lock.json file.
________________________________________
4.3.2. Updating Microservices to Use Shared Models
We'll update each microservice to utilize the shared models from the common library instead of defining their own.
a. Update auth-service
i. Update auth-service/models/User.js
Remove the existing model definition and use the shared model.
javascript
Copy code
// auth-service/models/User.js
const { User } = require('common');
module.exports = User;
ii. Update auth-service/controllers/authController.js
Ensure the controller uses the shared User model.
javascript
Copy code
// auth-service/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rabbitmqService = require('../services/rabbitmqService');
const { User, schemas } = require('common');
const { Validator } = require('jsonschema');
const validator = new Validator();
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
try {
// Check if user already exists
const existingUser = await User.findOne({ email: email.toLowerCase() });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = new User({
name,
email: email.toLowerCase(),
password: hashedPassword,
});
await newUser.save();
// Validate event against schema
const event = {
event: 'user_registered',
data: {
id: newUser._id.toString(),
name: newUser.name,
email: newUser.email,
},
};
const validation = validator.validate(event, schemas.userRegistered);
if (!validation.valid) {
console.error('Event validation failed:', validation.errors);
return res.status(500).json({ message: 'Invalid event structure' });
}
// Publish event to RabbitMQ
await rabbitmqService.publishEvent('user_events', event);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser._id, name, email: newUser.email } });
} catch (error) {
console.error('Error during registration:', error);
res.status(500).json({ message: 'Server error during registration' });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
// Basic validation
if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
try {
// Find user
const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Compare passwords
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create JWT
const token = jwt.sign({ id: user._id, email: user.email }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
});
res.status(200).json({ message: 'Login successful', token });
} catch (error) {
console.error('Error during login:', error);
res.status(500).json({ message: 'Server error during login' });
}
};
Explanation:
• Importing Shared Models and Schemas:
o const { User, schemas } = require('common');: Imports the User model and event schemas from the common library.
• Event Validation:
o jsonschema Validator: Validates the event against the userRegistered schema before publishing.
• Consistency:
o Ensures that the event structure adheres to the standardized schema.
iii. Update auth-service/routes/userRoutes.js
Assuming the /users route in auth-service is for admin purposes or user management.
javascript
Copy code
// auth-service/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const authenticate = require('../middleware/authenticate');
const { User } = require('common');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of users (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of users.
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.get('/', authenticate, async (req, res) => {
try {
const users = await User.find().select('-password'); // Exclude passwords
res.status(200).json(users);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ message: 'Server error fetching users' });
}
});
module.exports = router;
Explanation:
• Using Shared User Model: Fetches users from MongoDB using the standardized model.
• Security: Protected route using JWT authentication.
b. Update auth-service/services/rabbitmqService.js
No changes needed as the RabbitMQ service remains the same.
4.3.3. Update user-service
i. Update user-service/models/UserProfile.js
Assuming UserProfile is a separate model, it can remain as is, but if there are overlapping fields, ensure consistency.
If UserProfile references the User model from common, adjust accordingly.
javascript
Copy code
// user-service/models/UserProfile.js
const mongoose = require('mongoose');
const UserProfileSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
unique: true,
},
address: {
type: String,
trim: true,
},
phone: {
type: String,
trim: true,
},
// Add more profile-related fields as needed
}, { timestamps: true });
module.exports = mongoose.model('UserProfile', UserProfileSchema);
ii. Update user-service/controllers/userController.js
Modify to use shared models and validate incoming data if necessary.
javascript
Copy code
// user-service/controllers/userController.js
const { UserProfile, schemas } = require('common');
const { Validator } = require('jsonschema');
const validator = new Validator();
exports.getUsers = async (req, res) => {
try {
const profiles = await UserProfile.find().populate('userId', 'name email');
res.status(200).json(profiles);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ message: 'Server error fetching users' });
}
};
exports.getUserById = async (req, res) => {
const userId = req.params.id;
try {
const profile = await UserProfile.findOne({ userId }).populate('userId', 'name email');
if (profile) {
res.status(200).json(profile);
} else {
res.status(404).json({ message: 'User profile not found' });
}
} catch (error) {
console.error('Error fetching user by ID:', error);
res.status(500).json({ message: 'Server error fetching user' });
}
};
exports.createUser = async (req, res) => {
const { userId, address, phone } = req.body;
if (!userId) {
return res.status(400).json({ message: 'User ID is required' });
}
try {
// Check if profile already exists
const existingProfile = await UserProfile.findOne({ userId });
if (existingProfile) {
return res.status(400).json({ message: 'User profile already exists' });
}
const newProfile = new UserProfile({
userId,
address,
phone,
});
await newProfile.save();
res.status(201).json(newProfile);
} catch (error) {
console.error('Error creating user profile:', error);
res.status(500).json({ message: 'Server error creating user profile' });
}
};
Explanation:
• Using Shared Models: Utilizes UserProfile from the common library if applicable.
• Consistency: Ensures that any future changes to the UserProfile model in common propagate to all services.
Note: If UserProfile is unique to user-service, you may not need to include it in common. However, if multiple services interact with UserProfile, consider moving it to common.
iii. Update user-service/routes/userRoutes.js
javascript
Copy code
// user-service/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authenticate = require('../middleware/authenticate');
/**
* @swagger
* tags:
* name: Users
* description: User management
*/
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of user profiles (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of user profiles.
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.get('/', authenticate, userController.getUsers);
/**
* @swagger
* /users/{id}:
* get:
* summary: Get a user profile by User ID (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* description: User ID of the profile to retrieve
* schema:
* type: string
* responses:
* 200:
* description: User profile found
* 401:
* description: Unauthorized
* 404:
* description: User profile not found
* 500:
* description: Server error
*/
router.get('/:id', authenticate, userController.getUserById);
/**
* @swagger
* /users:
* post:
* summary: Create a new user profile (Protected)
* tags: [Users]
* security:
* - bearerAuth: []
* requestBody:
* description: User profile object that needs to be created
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UserProfileInput'
* responses:
* 201:
* description: User profile created successfully
* 400:
* description: Invalid input or profile already exists
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.post('/', authenticate, userController.createUser);
module.exports = router;
Explanation:
• Consistent API Documentation: Uses Swagger to document endpoints, ensuring clarity across services.
iv. Update user-service/tests/user.test.js
Ensure tests align with the new data models and utilize the shared common library.
javascript
Copy code
// user-service/tests/user.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const UserProfile = require('../models/UserProfile');
const { User } = require('common');
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Test User',
email: 'testuser@example.com',
password: 'testpassword',
});
// Extract user ID from register response
testUserId = registerRes.body.user.id;
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'testuser@example.com',
password: 'testpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await UserProfile.deleteMany({});
await User.deleteMany({});
await mongoose.connection.close();
});
describe('User Service API Endpoints', () => {
it('should create a new user profile', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId,
address: '123 Test Street',
phone: '123-456-7890',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUserId);
expect(res.body).toHaveProperty('address', '123 Test Street');
expect(res.body).toHaveProperty('phone', '123-456-7890');
});
it('should not create a user profile with existing userId', async () => {
// Attempt to create the same profile again
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId, // Same userId
address: '123 Test Street',
phone: '123-456-7890',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User profile already exists');
});
it('should retrieve a list of user profiles', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('userId');
expect(res.body[0].userId).toHaveProperty('name', 'Test User');
expect(res.body[0].userId).toHaveProperty('email', 'testuser@example.com');
});
it('should retrieve a user profile by user ID', async () => {
const res = await request(app)
.get(`/users/${testUserId}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('userId');
expect(res.body.userId).toHaveProperty('name', 'Test User');
expect(res.body.userId).toHaveProperty('email', 'testuser@example.com');
});
it('should not retrieve a non-existent user profile', async () => {
const res = await request(app)
.get('/users/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'User profile not found');
});
});
Explanation:
• Utilizing Shared Models: Uses User from common to manage user data.
• Dynamic userId: Extracts userId from the registration response to ensure tests are dynamic and not hardcoded.
Note: Ensure that the Auth Service is running and accessible during tests.
c. Create common/schemas/userProfile.json
If UserProfile needs a standardized schema, define it in common.
json
Copy code
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UserProfile",
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "User ID referencing the User model"
},
"address": {
"type": "string",
"description": "User's address"
},
"phone": {
"type": "string",
"description": "User's phone number"
}
// Add more fields as needed
},
"required": ["userId"],
"additionalProperties": false
}
________________________________________
4.3.4. Update product-service
i. Update product-service/models/Product.js
Use the shared Product model from common.
javascript
Copy code
// product-service/models/Product.js
const { Product } = require('common');
module.exports = Product;
ii. Update product-service/controllers/productController.js
Ensure controllers use shared models and validate events.
javascript
Copy code
// product-service/controllers/productController.js
const { Product, schemas } = require('common');
const rabbitmqService = require('../services/rabbitmqService');
const { Validator } = require('jsonschema');
const validator = new Validator();
exports.getProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json(products);
} catch (error) {
console.error('Error fetching products:', error);
res.status(500).json({ message: 'Server error fetching products' });
}
};
exports.getProductById = async (req, res) => {
const productId = req.params.id;
try {
const product = await Product.findById(productId);
if (product) {
res.status(200).json(product);
} else {
res.status(404).json({ message: 'Product not found' });
}
} catch (error) {
console.error('Error fetching product by ID:', error);
res.status(500).json({ message: 'Server error fetching product' });
}
};
exports.createProduct = async (req, res) => {
const { name, price, description, inventory } = req.body;
if (!name || price === undefined) {
return res.status(400).json({ message: 'Name and price are required' });
}
try {
const newProduct = new Product({
name,
price,
description,
inventory: inventory || 0,
});
await newProduct.save();
// Validate event against schema
const event = {
event: 'product_created',
data: {
id: newProduct._id.toString(),
name: newProduct.name,
price: newProduct.price,
},
};
const validation = validator.validate(event, schemas.productCreated);
if (!validation.valid) {
console.error('Event validation failed:', validation.errors);
return res.status(500).json({ message: 'Invalid event structure' });
}
// Publish event to RabbitMQ
await rabbitmqService.publishEvent('product_events', event);
res.status(201).json(newProduct);
} catch (error) {
console.error('Error creating product:', error);
res.status(500).json({ message: 'Server error creating product' });
}
};
Explanation:
• Using Shared Models: Utilizes Product from common.
• Event Validation: Ensures that the product_created event adheres to the standardized schema before publishing.
iii. Update product-service/routes/productRoutes.js
javascript
Copy code
// product-service/routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
const authenticate = require('../middleware/authenticate'); // If product routes are protected
/**
* @swagger
* tags:
* name: Products
* description: Product management
*/
/**
* @swagger
* /products:
* get:
* summary: Retrieve a list of products
* tags: [Products]
* responses:
* 200:
* description: A list of products.
* 500:
* description: Server error
*/
router.get('/', productController.getProducts);
/**
* @swagger
* /products/{id}:
* get:
* summary: Get a product by ID
* tags: [Products]
* parameters:
* - in: path
* name: id
* required: true
* description: ID of the product to retrieve
* schema:
* type: string
* responses:
* 200:
* description: Product found
* 404:
* description: Product not found
* 500:
* description: Server error
*/
router.get('/:id', productController.getProductById);
/**
* @swagger
* /products:
* post:
* summary: Create a new product
* tags: [Products]
* requestBody:
* description: Product object that needs to be added
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ProductInput'
* responses:
* 201:
* description: Product created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', productController.createProduct);
module.exports = router;
Explanation:
• Consistent API Documentation: Uses Swagger to document endpoints, ensuring clarity and consistency.
iv. Update product-service/tests/product.test.js
Ensure tests align with the shared models and utilize the standardized schemas.
javascript
Copy code
// product-service/tests/product.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const Product = require('../models/Product');
const { User } = require('common');
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Product Tester',
email: 'producttester@example.com',
password: 'productpassword',
});
// Extract user ID from register response
testUserId = registerRes.body.user.id;
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'producttester@example.com',
password: 'productpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await Product.deleteMany({});
await User.deleteMany({});
await mongoose.connection.close();
});
describe('Product Service API Endpoints', () => {
it('should create a new product', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Test Product',
price: 99.99,
description: 'A product for testing purposes',
inventory: 100,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
expect(res.body).toHaveProperty('description', 'A product for testing purposes');
expect(res.body).toHaveProperty('inventory', 100);
});
it('should not create a product without name and price', async () => {
const res = await request(app)
.post('/products')
.send({
description: 'Missing name and price',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Name and price are required');
});
it('should retrieve a list of products', async () => {
const res = await request(app)
.get('/products');
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Test Product');
});
it('should retrieve a product by ID', async () => {
// Fetch the product created earlier
const products = await Product.find().limit(1);
const productId = products[0]._id.toString();
const res = await request(app)
.get(`/products/${productId}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
});
it('should not retrieve a non-existent product', async () => {
const res = await request(app)
.get('/products/60c72b2f9b1e8e1d4c8e4f8d'); // Assuming this ID doesn't exist
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Product not found');
});
});
Explanation:
• Utilizing Shared Models: Uses Product from common to manage product data.
• Dynamic Data: Avoids hardcoding IDs by extracting them from responses.
________________________________________
4.4. Updating Notification Service
If the Notification Service needs to store notifications or logs, integrate it with the common library. For this stage, we'll assume it only logs events, so no changes are required. However, if future requirements arise, you can follow similar steps to integrate shared models.
________________________________________
4.5. Updating API Gateway
The API Gateway typically handles routing and may not directly interact with data models. However, if it requires access to user data for rate limiting or analytics, consider integrating the common library. For this stage, no changes are necessary.
________________________________________
5. Complete Code and Configuration Files
5.1. Root package.json
(Already provided above in section 4.1.1)
5.2. Common Library (common/)
common/package.json
(Already provided above in section 4.2.4)
common/models/User.js
(Already provided above in section 4.2.3.a)
common/models/Product.js
(Already provided above in section 4.2.3.b)
common/schemas/userRegistered.json
(Already provided above in section 4.2.4.a)
common/schemas/productCreated.json
(Already provided above in section 4.2.4.b)
common/index.js
(Already provided above in section 4.2.5.d)
common/README.md
markdown
Copy code
# Common Library for iBranDIDit.com Microservices
This library contains shared data models and event schemas used across all microservices in the iBranDIDit.com architecture.
## Models
- **User:** Defines the structure for user data.
- **Product:** Defines the structure for product data.
## Schemas
- **User Registered Event:** Schema for `user_registered` events.
- **Product Created Event:** Schema for `product_created` events.
## Usage
Install the library in your microservice:
```bash
npm install common@1.0.0
Import models and schemas as needed:
javascript
Copy code
const { User, Product, schemas } = require('common');
Contributing
Ensure that any changes to models or schemas maintain backward compatibility to prevent breaking existing services.
makefile
Copy code
---
### **5.3. Authentication Service (`auth-service/`)**
#### **`auth-service/package.json`**
*(Already provided in Stage 5, updated to include `common`)*
```json
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
auth-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
auth-service/models/User.js
(Already updated in section 4.3.2.a)
javascript
Copy code
// auth-service/models/User.js
const { User } = require('common');
module.exports = User;
auth-service/controllers/authController.js
(Already updated in section 4.3.2.b)
auth-service/routes/authRoutes.js
(Already updated in section 4.3.2.b)
auth-service/middleware/authenticate.js
(Assuming it's already present from previous stages. If not, create it as shown in previous stages.)
auth-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure it remains compatible with shared models.)
auth-service/tests/auth.test.js
(Already updated in section 4.3.2.c)
auth-service/swagger.js
Ensure that Swagger documentation aligns with the updated models and schemas.
javascript
Copy code
// auth-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Authentication Service API',
version: '1.0.0',
description: 'API documentation for the Authentication Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5003',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
RegisterInput: {
type: 'object',
required: ['name', 'email', 'password'],
properties: {
name: {
type: 'string',
description: 'The name of the user',
},
email: {
type: 'string',
description: 'The email of the user',
},
password: {
type: 'string',
description: 'The password of the user',
},
},
example: {
name: 'Alice',
email: 'alice@example.com',
password: 'securepassword',
},
},
LoginInput: {
type: 'object',
required: ['email', 'password'],
properties: {
email: {
type: 'string',
description: 'The email of the user',
},
password: {
type: 'string',
description: 'The password of the user',
},
},
example: {
email: 'alice@example.com',
password: 'securepassword',
},
},
UserOutput: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'User ID',
},
name: {
type: 'string',
description: 'User name',
},
email: {
type: 'string',
description: 'User email',
},
},
},
LoginOutput: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Login message',
},
token: {
type: 'string',
description: 'JWT token',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
Explanation:
• Security Schemes: Defines bearerAuth for JWT authentication.
• Schemas: Documents input and output schemas for registration and login.
________________________________________
5.4. User Service (user-service/)
user-service/package.json
(Already provided in Stage 5, updated to include common)
json
Copy code
{
"name": "user-service",
"version": "1.0.0",
"description": "User Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
user-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5001
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/userdb?authSource=admin
user-service/models/UserProfile.js
(Already provided in section 4.3.3.i)
javascript
Copy code
// user-service/models/UserProfile.js
const mongoose = require('mongoose');
const UserProfileSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
unique: true,
},
address: {
type: String,
trim: true,
},
phone: {
type: String,
trim: true,
},
// Add more profile-related fields as needed
}, { timestamps: true });
module.exports = mongoose.model('UserProfile', UserProfileSchema);
Note: If UserProfile is unique to user-service, no changes are needed. Otherwise, consider moving it to common if shared.
user-service/index.js
(Already provided in section 4.3.3.b)
user-service/controllers/userController.js
(Already provided in section 4.3.3.b)
user-service/routes/userRoutes.js
(Already provided in section 4.3.3.c)
user-service/middleware/authenticate.js
(Assuming it's already present from previous stages. If not, create it as shown in previous stages.)
user-service/services/rabbitmqService.js
If the User Service needs to publish events, implement similar to auth-service. For now, assuming it only consumes or manages user profiles, no changes required.
user-service/tests/user.test.js
(Already provided in section 4.3.3.d)
user-service/swagger.js
Ensure Swagger documentation aligns with the updated models and schemas.
javascript
Copy code
// user-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com User Service API',
version: '1.0.0',
description: 'API documentation for the User Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5001',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
UserProfileInput: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string',
description: 'User ID referencing the User model',
},
address: {
type: 'string',
description: 'User address',
},
phone: {
type: 'string',
description: 'User phone number',
},
},
example: {
userId: '60d5f9f9b1e8e1d4c8e4f8c5',
address: '123 Test Street',
phone: '123-456-7890',
},
},
UserProfileOutput: {
type: 'object',
properties: {
_id: {
type: 'string',
description: 'UserProfile ID',
},
userId: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
address: {
type: 'string',
description: 'User address',
},
phone: {
type: 'string',
description: 'User phone number',
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation timestamp',
},
updatedAt: {
type: 'string',
format: 'date-time',
description: 'Update timestamp',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
Explanation:
• Security Schemes: Defines bearerAuth for JWT authentication.
• Schemas: Documents input and output schemas for user profiles.
user-service/Dockerfile
(Already provided in section 4.3.3.f)
dockerfile
Copy code
# user-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5001
CMD [ "npm", "start" ]
________________________________________
5.5. Product Service (product-service/)
product-service/package.json
(Already provided in Stage 5, updated to include common)
json
Copy code
{
"name": "product-service",
"version": "1.0.0",
"description": "Product Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
product-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5002
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
product-service/models/Product.js
(Already updated in section 4.3.3.a)
javascript
Copy code
// product-service/models/Product.js
const { Product } = require('common');
module.exports = Product;
product-service/controllers/productController.js
(Already updated in section 4.3.3.b)
product-service/routes/productRoutes.js
(Already updated in section 4.3.3.c)
product-service/middleware/authenticate.js
(If required, similar to other services. Otherwise, no changes needed.)
product-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure it remains compatible with shared models.)
product-service/tests/product.test.js
(Already provided in section 4.3.3.d)
product-service/swagger.js
Ensure Swagger documentation aligns with the updated models and schemas.
javascript
Copy code
// product-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Product Service API',
version: '1.0.0',
description: 'API documentation for the Product Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5002',
description: 'Development server',
},
],
components: {
schemas: {
ProductInput: {
type: 'object',
required: ['name', 'price'],
properties: {
name: {
type: 'string',
description: 'Product name',
},
price: {
type: 'number',
description: 'Product price',
},
description: {
type: 'string',
description: 'Product description',
},
inventory: {
type: 'number',
description: 'Number of items in stock',
},
},
example: {
name: 'Sneakers',
price: 49.99,
description: 'Comfortable running sneakers',
inventory: 150,
},
},
ProductOutput: {
type: 'object',
properties: {
_id: {
type: 'string',
description: 'Product ID',
},
name: {
type: 'string',
description: 'Product name',
},
price: {
type: 'number',
description: 'Product price',
},
description: {
type: 'string',
description: 'Product description',
},
inventory: {
type: 'number',
description: 'Number of items in stock',
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation timestamp',
},
updatedAt: {
type: 'string',
format: 'date-time',
description: 'Update timestamp',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
________________________________________
5.6. Notification Service (notification-service/)
Since the Notification Service primarily consumes events and logs actions, integrating it with the common library is optional. However, if you plan to store notifications or logs, consider utilizing shared models.
notification-service/package.json
(Already provided in Stage 4, updated to include common if needed)
json
Copy code
{
"name": "notification-service",
"version": "1.0.0",
"description": "Notification Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node"
}
}
Note: If the Notification Service requires shared models or schemas, ensure to utilize the common library accordingly.
notification-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5004
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/notificationdb?authSource=admin
notification-service/index.js
(Already provided in Stage 4, ensure compatibility with common)
javascript
Copy code
// notification-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const notificationRoutes = require('./routes/notificationRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB (if needed)
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Notification Service connected to MongoDB'))
.catch((err) => console.error('Notification Service failed to connect to MongoDB:', err));
// Routes
app.use('/notifications', notificationRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Notification Service API');
});
// Start Server
const PORT = process.env.PORT || 5004;
app.listen(PORT, () => {
console.log(`Notification Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
// Initialize RabbitMQ Consumer
const rabbitmqService = require('./services/rabbitmqService');
module.exports = app; // Export app for testing
Explanation:
• Connection to MongoDB: If the Notification Service needs to store notifications, ensure the MongoDB connection is active.
• Using common: If utilizing shared models, import them as needed.
notification-service/controllers/notificationController.js
(Optional: Implement if you plan to expose APIs for notifications.)
notification-service/routes/notificationRoutes.js
(Optional: Implement if you plan to expose APIs for notifications.)
notification-service/services/rabbitmqService.js
(Already provided in Stage 4, ensure it remains compatible with shared models if needed.)
notification-service/tests/notification.test.js
(Already provided in Stage 4)
notification-service/swagger.js
(Already provided in Stage 4)
notification-service/Dockerfile
(Already provided in Stage 4)
________________________________________
5.7. API Gateway (api-gateway/)
The API Gateway typically manages routing and authentication. It doesn't directly interact with data models, but ensure that it correctly handles JWTs and routes requests to the appropriate services.
api-gateway/package.json
(Already provided in Stage 4, updated to include common if needed)
json
Copy code
{
"name": "api-gateway",
"version": "1.0.0",
"description": "API Gateway for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "express-gateway start"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express-gateway": "^2.30.0",
"jsonwebtoken": "^9.0.0",
"amqplib": "^0.10.3",
"common": "1.0.0" // If needed
}
}
Note: If the API Gateway requires access to shared models or schemas, utilize the common library accordingly.
api-gateway/config/gateway.config.yml
(Already provided in Stage 4, ensure JWT_SECRET matches auth-service.)
yaml
Copy code
# api-gateway/config/gateway.config.yml
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- jwt-auth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- jwt-auth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
Important:
• Replace 'your_jwt_secret_key' with the actual JWT_SECRET from auth-service/.env.
api-gateway/policies/jwt-auth/jwt-auth.js
(Already provided in Stage 4, ensure compatibility with shared models if needed.)
api-gateway/Dockerfile
(Already provided in Stage 4)
________________________________________
6. Running the Project
6.1. Install Dependencies
From the root directory (ibrandidit-microservices/), install all dependencies across workspaces.
bash
Copy code
npm install
Explanation:
• NPM Workspaces will install dependencies for common and all microservices, ensuring proper linking.
6.2. Start All Services
Build and start all services using Docker Compose.
bash
Copy code
docker-compose up --build
Expected Output:
bash
Copy code
Creating network "ibrandidit-microservices_backend" with the default driver
Creating ibrandidit-microservices_common_1 ... done
Creating ibrandidit-microservices_rabbitmq_1 ... done
Creating ibrandidit-microservices_mongodb_1 ... done
Creating ibrandidit-microservices_auth-service_1 ... done
Creating ibrandidit-microservices_user-service_1 ... done
Creating ibrandidit-microservices_product-service_1 ... done
Creating ibrandidit-microservices_notification-service_1 ... done
Creating ibrandidit-microservices_api-gateway_1 ... done
Note: Ensure all services start without errors. If any service fails to connect to MongoDB or RabbitMQ, check the environment variables and service dependencies.
________________________________________
7. Documentation and Usage
7.1. Accessing MongoDB
• MongoDB Management UI:
o Option 1: Use MongoDB Compass (GUI tool) to connect to your local MongoDB instance.
Connection String: mongodb://root:example@localhost:27017
o Option 2: Use Mongo Shell.
Command:
bash
Copy code
mongo -u root -p example --authenticationDatabase admin
7.2. Accessing RabbitMQ Management UI
• URL: http://localhost:15672
• Username: guest
• Password: guest
• Usage:
o Queues: Monitor user_events and product_events queues.
o Exchanges: Observe message routing.
o Messages: View message details or manually publish messages for testing.
7.3. Accessing Service API Documentation
Each microservice has its own Swagger documentation:
• Authentication Service: http://localhost:5003/api-docs
• User Service: http://localhost:5001/api-docs
• Product Service: http://localhost:5002/api-docs
• Notification Service: http://localhost:5004/api-docs
Usage:
• Explore Endpoints: Use Swagger UI to interact with API endpoints.
• Understand Schemas: View request and response schemas for each endpoint.
7.4. Workflow Summary
1. User Registration:
o Action: User registers via the Auth Service (/auth/register).
o Event Published: user_registered to user_events queue.
o Notification Service: Listens to user_events and sends a welcome email.
2. User Login:
o Action: User logs in via the Auth Service (/auth/login).
o Response: Receives a JWT for authenticated requests.
3. User Profile Management:
o Action: Authenticated users can create and manage their profiles via the User Service (/users).
4. Product Management:
o Action: Create, retrieve, and manage products via the Product Service (/products).
o Event Published: product_created to product_events queue.
o Notification Service: Listens to product_events and updates inventory.
5. API Gateway:
o Action: Routes requests to respective services.
o Authentication: Validates JWTs for protected routes using the jwt-auth policy.
7.5. Deployment Considerations
• Environment Variables: Ensure all environment variables in .env files are correctly set, especially sensitive information like JWT_SECRET and MONGODB_URI.
• Volumes: Data in MongoDB is persisted using Docker volumes (mongo-data). Ensure volume management is handled appropriately.
• Scaling Services: Utilize Docker Compose's scaling features to handle increased load (e.g., multiple instances of user-service).
bash
Copy code
docker-compose up --scale user-service=3 -d
________________________________________
8. Testing Services
8.1. Running Tests
Each service has its own set of tests. To run tests for a specific service:
bash
Copy code
# Navigate to the service directory
cd auth-service
npm test
cd ../user-service
npm test
cd ../product-service
npm test
cd ../notification-service
npm test
# Similarly for other services
Example Output:
bash
Copy code
PASS auth-service/tests/auth.test.js
Authentication API Endpoints
✓ should register a new user (XXX ms)
✓ should not register a user with existing email (XXX ms)
✓ should login an existing user (XXX ms)
✓ should not login with incorrect password (XXX ms)
✓ should not login a non-existent user (XXX ms)
✓ should retrieve a list of users with valid token (XXX ms)
✓ should not retrieve users without token (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: X.XXX s
8.2. Manual Testing
1. User Registration and Event Publishing:
o Register a New User:
http
Copy code
POST http://localhost:5003/auth/register
Content-Type: application/json
{
"name": "Frank",
"email": "frank@example.com",
"password": "frankspassword"
}
Expected Response:
json
Copy code
{
"message": "User registered successfully",
"user": {
"id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Frank",
"email": "frank@example.com"
}
}
o Verify Event in RabbitMQ:
Open RabbitMQ Management UI.
Navigate to the user_events queue.
Confirm that a user_registered event is present.
o Check Notification Service Logs:
Observe logs for actions like sending a welcome email.
2. Product Creation and Event Publishing:
o Create a New Product:
http
Copy code
POST http://localhost:5002/products
Content-Type: application/json
{
"name": "Sneakers",
"price": 49.99,
"description": "Comfortable running sneakers",
"inventory": 150
}
Expected Response:
json
Copy code
{
"_id": "60d5fb0ab1e8e1d4c8e4f8c7",
"name": "Sneakers",
"price": 49.99,
"description": "Comfortable running sneakers",
"inventory": 150,
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
o Verify Event in RabbitMQ:
Open RabbitMQ Management UI.
Navigate to the product_events queue.
Confirm that a product_created event is present.
o Check Notification Service Logs:
Observe logs for actions like updating inventory.
3. API Gateway Authentication:
o Attempt to Access Protected Routes Without a Token:
http
Copy code
GET http://localhost:8080/users
Expected Response:
json
Copy code
{
"message": "Access token missing"
}
o Access Protected Routes with a Valid Token:
http
Copy code
GET http://localhost:8080/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
Expected Response:
json
Copy code
[
{
"_id": "60d5f9f9b1e8e1d4c8e4f8c5",
"name": "Frank",
"email": "frank@example.com",
"createdAt": "2023-08-01T00:00:00.000Z",
"updatedAt": "2023-08-01T00:00:00.000Z",
"__v": 0
}
]
________________________________________
9. Troubleshooting Tips
1. Common Library Issues:
o Module Not Found:
Ensure that common is correctly installed in each microservice.
Verify that NPM Workspaces are properly configured.
Run npm install from the root directory to ensure all dependencies are linked.
o Version Mismatch:
Ensure that all microservices depend on the correct version of common.
Update the version in common/package.json and reinstall dependencies if changes are made.
2. MongoDB Connection Issues:
o Verify Connection String:
Ensure MONGODB_URI in each microservice's .env file is correct.
Example: mongodb://root:example@mongodb:27017/authdb?authSource=admin
o Check MongoDB Logs:
bash
Copy code
docker-compose logs mongodb
o Ensure MongoDB is Running:
Confirm that the MongoDB service is up via Docker Compose.
bash
Copy code
docker-compose ps
3. RabbitMQ Connection Issues:
o Verify Connection String:
Ensure RABBITMQ_URL in each microservice's .env file is correct.
Example: amqp://guest:guest@rabbitmq:5672
o Check RabbitMQ Logs:
bash
Copy code
docker-compose logs rabbitmq
o Ensure RabbitMQ is Running:
Confirm that the RabbitMQ service is up via Docker Compose.
bash
Copy code
docker-compose ps
4. Service Communication Issues:
o Check Docker Networks:
Ensure all services are connected to the backend network.
bash
Copy code
docker network inspect ibrandidit-microservices_backend
o Ping Services Internally:
Use Docker exec to ping services.
bash
Copy code
docker exec -it ibrandidit-microservices_auth-service_1 ping user-service
5. JWT Authentication Issues:
o Secret Key Mismatch:
Ensure JWT_SECRET in auth-service/.env matches the one in api-gateway/config/gateway.config.yml.
o Token Expiry:
Ensure tokens are not expired when making requests.
Check JWT_EXPIRES_IN in auth-service/.env.
6. Swagger UI Not Loading:
o Service Running: Ensure the respective service is up and accessible.
o Correct Paths: Verify that Swagger setup points to the correct route and controller files.
o Check Service Logs: Look for errors related to Swagger setup.
bash
Copy code
docker-compose logs auth-service
7. Testing Failures:
o Ensure Services Are Running: Some tests may fail if dependent services are not up.
o Check Environment Variables: Ensure all required variables are set correctly.
o Database State: Clean the database before running tests to avoid conflicts.
8. Docker Compose Issues:
o Rebuild Containers:
bash
Copy code
docker-compose up --build
o Restart Services:
bash
Copy code
docker-compose restart
o Check Resource Allocation: Ensure Docker has enough resources allocated on your machine.
9. Event Handling Issues:
o Check Queues in RabbitMQ: Ensure messages are being published to the correct queues.
o Service Logs: Inspect logs for auth-service, product-service, and notification-service for any errors related to RabbitMQ.
________________________________________
10. Next Steps
With Stage 6 completed, you've successfully established consistent data models across your microservices architecture. This foundational step ensures data integrity and seamless communication between services, setting the stage for more advanced features.
Proceed to Stage 7: Implementing User Roles and Permissions
In the next stage, we'll introduce user roles (e.g., Admin, Customer) and implement role-based access control (RBAC) to secure your platform effectively.
________________________________________
11. Additional Resources
• NPM Workspaces Documentation: https://docs.npmjs.com/cli/v7/using-npm/workspaces
• MongoDB Documentation: https://docs.mongodb.com/
• Mongoose Documentation: https://mongoosejs.com/docs/
• JSON Schema Documentation: https://json-schema.org/
• RabbitMQ Documentation: https://www.rabbitmq.com/documentation.html
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• Swagger Documentation: https://swagger.io/docs/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• Docker Documentation: https://docs.docker.com/
• Docker Compose Documentation: https://docs.docker.com/compose/
________________________________________
Disclaimer: This guide provides a foundational setup for maintaining consistent data models across a microservices architecture. Depending on project complexity and requirements, further optimizations and configurations may be necessary, such as implementing versioning for models, advanced validation, and integrating with CI/CD pipelines for automated testing and deployment.
________________________________________
By following this comprehensive guide for Stage 6: Consistent Data Models, you've ensured that all microservices in your architecture operate with unified and standardized data structures. This alignment is crucial for maintaining data integrity, simplifying inter-service communication, and facilitating future scalability and maintainability of your eCommerce platform.
Stage 7: Comprehensive Testing for your eCommerce platform, iBranDIDit.com. In this stage, we'll implement a thorough testing strategy across all microservices to ensure reliability, maintainability, and robustness of your application. This guide is tailored for beginners, providing step-by-step instructions, complete code examples, and necessary configurations.
________________________________________
Stage 7: Comprehensive Testing
________________________________________
1. Overview
Objective:
• Implement Comprehensive Testing: Establish a robust testing framework encompassing unit tests, integration tests, and end-to-end (E2E) tests for all microservices.
• Ensure Code Quality: Detect and prevent bugs early in the development cycle.
• Facilitate Maintenance: Simplify future updates and feature additions by ensuring existing functionalities remain unaffected.
• Enhance Reliability: Guarantee that each service performs as expected individually and in conjunction with other services.
Benefits:
• Early Bug Detection: Identifying issues during development rather than post-deployment.
• Documentation of Functionality: Tests serve as additional documentation, illustrating how components are expected to behave.
• Confidence in Deployments: Automated tests provide assurance that new changes don't break existing functionalities.
• Streamlined Development Workflow: Facilitates continuous integration and deployment (CI/CD) practices in future stages.
________________________________________
2. Technology Stack
• Testing Framework: Jest – A delightful JavaScript Testing Framework with a focus on simplicity.
• HTTP Assertions: Supertest – A library for testing Node.js HTTP servers.
• Mocking Libraries: Jest Mock – Built-in mocking capabilities in Jest.
• In-Memory Databases (for testing): MongoDB Memory Server – Provides an in-memory MongoDB instance for testing.
• Continuous Integration (Optional for future stages): Tools like GitHub Actions or Jenkins can be integrated later.
________________________________________
3. Project Structure
We'll enhance each microservice's tests/ directory to include both unit and integration tests. The common library will also be tested to ensure shared models and schemas function correctly.
lua
Copy code
ibrandidit-microservices/
├── common/
│ ├── models/
│ │ ├── User.js
│ │ └── Product.js
│ ├── schemas/
│ │ ├── userRegistered.json
│ │ └── productCreated.json
│ ├── __tests__/
│ │ ├── User.test.js
│ │ └── Product.test.js
│ ├── package.json
│ └── README.md
├── api-gateway/
│ ├── config/
│ │ └── gateway.config.yml
│ ├── policies/
│ │ └── jwt-auth/
│ │ └── jwt-auth.js
│ ├── Dockerfile
│ ├── package.json
│ ├── __tests__/
│ │ └── gateway.test.js
│ └── README.md
├── auth-service/
│ ├── controllers/
│ │ └── authController.js
│ ├── middleware/
│ │ └── authenticate.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ ├── authRoutes.js
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ ├── auth.test.js
│ │ └── user.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── user-service/
│ ├── controllers/
│ │ └── userController.js
│ ├── models/
│ │ └── UserProfile.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ ├── user.test.js
│ │ └── userProfile.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ │ └── productController.js
│ ├── models/
│ │ └── Product.js
│ ├── routes/
│ │ └── productRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ ├── product.test.js
│ │ └── productIntegration.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── notification-service/
│ ├── controllers/
│ │ └── notificationController.js
│ ├── routes/
│ │ └── notificationRoutes.js
│ ├── services/
│ │ └── rabbitmqService.js
│ ├── tests/
│ │ └── notification.test.js
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
├── package.json
└── README.md
Explanation:
• __tests__/ Directory: Contains unit and integration tests for the common library.
• tests/ Directory in Each Service: Houses tests specific to each microservice, organized by functionality.
• Consistent Naming Conventions: Test files are named descriptively (e.g., auth.test.js, userProfile.test.js) for clarity.
________________________________________
4. Implementation Steps
4.1. Setting Up Testing Frameworks
Each microservice and the common library will be configured with Jest and Supertest for testing.
4.1.1. Installing Testing Dependencies
Navigate to each microservice and the common library to install necessary testing packages.
bash
Copy code
# Navigate to the common library
cd common
npm install --save-dev jest
npm install --save-dev @types/jest
npm install --save-dev supertest
# Repeat for each microservice
cd ../auth-service
npm install --save-dev jest @types/jest supertest
cd ../user-service
npm install --save-dev jest @types/jest supertest
cd ../product-service
npm install --save-dev jest @types/jest supertest
cd ../notification-service
npm install --save-dev jest @types/jest supertest
cd ../api-gateway
npm install --save-dev jest @types/jest supertest
Note: For simplicity, common uses Jest for testing models and schemas.
4.1.2. Configuring Jest
Create a jest.config.js file in each microservice and the common library to define Jest configurations.
Example: auth-service/jest.config.js
javascript
Copy code
// auth-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
};
Example: common/jest.config.js
javascript
Copy code
// common/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
};
Creating jest.setup.js (if needed)
If any setup is required before tests run (e.g., connecting to an in-memory database), create a jest.setup.js file.
javascript
Copy code
// auth-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
Note: For integration tests involving databases, consider using MongoDB Memory Server to run tests without affecting the development or production databases.
________________________________________
4.2. Writing Unit Tests
Unit Tests focus on individual components, such as controllers and models, ensuring they function correctly in isolation.
4.2.1. Testing the common Library
a. common/__tests__/User.test.js
javascript
Copy code
// common/__tests__/User.test.js
const mongoose = require('mongoose');
const { User } = require('../models/User');
describe('User Model Test', () => {
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/common_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
it('create & save user successfully', async () => {
const validUser = new User({
name: 'Test User',
email: 'testuser@example.com',
password: 'password123',
});
const savedUser = await validUser.save();
// Object Id should be defined when successfully saved to MongoDB.
expect(savedUser._id).toBeDefined();
expect(savedUser.name).toBe(validUser.name);
expect(savedUser.email).toBe(validUser.email);
expect(savedUser.password).toBe(validUser.password);
});
it('insert user successfully, but the field not defined in schema should be undefined', async () => {
const userWithInvalidField = new User({
name: 'Invalid User',
email: 'invaliduser@example.com',
password: 'password123',
nickname: 'Tester', // This field is not defined in the schema
});
const savedUser = await userWithInvalidField.save();
expect(savedUser._id).toBeDefined();
expect(savedUser.nickname).toBeUndefined();
});
it('create user without required field should fail', async () => {
const userWithoutRequiredField = new User({ name: 'Incomplete User' });
let err;
try {
const savedUser = await userWithoutRequiredField.save();
error = savedUser;
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
expect(err.errors.email).toBeDefined();
expect(err.errors.password).toBeDefined();
});
});
b. common/__tests__/Product.test.js
javascript
Copy code
// common/__tests__/Product.test.js
const mongoose = require('mongoose');
const { Product } = require('../models/Product');
describe('Product Model Test', () => {
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/common_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
it('create & save product successfully', async () => {
const validProduct = new Product({
name: 'Test Product',
price: 99.99,
description: 'A product for testing purposes',
inventory: 50,
});
const savedProduct = await validProduct.save();
expect(savedProduct._id).toBeDefined();
expect(savedProduct.name).toBe(validProduct.name);
expect(savedProduct.price).toBe(validProduct.price);
expect(savedProduct.description).toBe(validProduct.description);
expect(savedProduct.inventory).toBe(validProduct.inventory);
});
it('create product without required fields should fail', async () => {
const productWithoutRequiredField = new Product({ description: 'No name and price' });
let err;
try {
const savedProduct = await productWithoutRequiredField.save();
error = savedProduct;
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
expect(err.errors.name).toBeDefined();
expect(err.errors.price).toBeDefined();
});
});
Explanation:
• beforeAll and afterAll: Connect to an in-memory or test MongoDB instance before tests and clean up afterward.
• Test Cases:
o Successful Creation: Ensures that valid data saves correctly.
o Ignoring Undefined Fields: Verifies that fields not defined in the schema are not saved.
o Validation Errors: Checks that missing required fields throw appropriate errors.
4.2.2. Testing Microservices
Each microservice's unit tests will focus on controllers, ensuring they handle requests and responses correctly.
a. auth-service/tests/auth.test.js
javascript
Copy code
// auth-service/tests/auth.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { User } = require('common');
const bcrypt = require('bcrypt');
describe('Authentication Service API Endpoints', () => {
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/auth_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await User.deleteMany({});
await mongoose.connection.close();
});
describe('POST /auth/register', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Alice',
email: 'alice@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'User registered successfully');
expect(res.body).toHaveProperty('user');
expect(res.body.user).toHaveProperty('id');
expect(res.body.user).toHaveProperty('name', 'Alice');
expect(res.body.user).toHaveProperty('email', 'alice@example.com');
// Verify password is hashed
const user = await User.findOne({ email: 'alice@example.com' });
expect(user).toBeDefined();
const isPasswordHashed = await bcrypt.compare('securepassword', user.password);
expect(isPasswordHashed).toBe(true);
});
it('should not register a user with existing email', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Alice Duplicate',
email: 'alice@example.com',
password: 'anotherpassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User already exists');
});
it('should fail registration with missing fields', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Bob',
// Missing email and password
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Name, email, and password are required');
});
});
describe('POST /auth/login', () => {
it('should login an existing user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'alice@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Login successful');
expect(res.body).toHaveProperty('token');
expect(typeof res.body.token).toBe('string');
});
it('should not login with incorrect password', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'alice@example.com',
password: 'wrongpassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
it('should not login a non-existent user', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'securepassword',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid credentials');
});
});
describe('GET /users', () => {
let token;
beforeAll(async () => {
// Login to get the token
const loginRes = await request(app)
.post('/auth/login')
.send({
email: 'alice@example.com',
password: 'securepassword',
});
token = loginRes.body.token;
});
it('should retrieve a list of users with valid token', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('name', 'Alice');
expect(res.body[0]).toHaveProperty('email', 'alice@example.com');
expect(res.body[0]).not.toHaveProperty('password'); // Password should be excluded
});
it('should not retrieve users without token', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Access token missing');
});
it('should not retrieve users with invalid token', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', 'Bearer invalidtoken');
expect(res.statusCode).toEqual(403);
expect(res.body).toHaveProperty('message', 'Invalid or expired token');
});
});
});
Explanation:
• Test Lifecycle Hooks:
o beforeAll: Connects to a test database before running tests.
o afterAll: Cleans up the database and closes the connection after tests.
• Test Suites:
o Registration Tests: Validates successful registration, duplicate registrations, and missing fields.
o Login Tests: Checks successful login, incorrect password, and non-existent user scenarios.
o Protected Route Tests (/users): Ensures that authenticated access works and unauthorized access is handled correctly.
• Password Verification: Confirms that passwords are hashed before storage.
b. auth-service/tests/user.test.js
javascript
Copy code
// auth-service/tests/user.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { User } = require('common');
describe('User Service API Endpoints', () => {
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/user_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user
const testUser = new User({
name: 'Charlie',
email: 'charlie@example.com',
password: 'charliespassword',
});
await testUser.save();
});
afterAll(async () => {
// Clean up database after tests
await User.deleteMany({});
await mongoose.connection.close();
});
describe('GET /users', () => {
let token;
beforeAll(async () => {
// Login to get the token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'charlie@example.com',
password: 'charliespassword',
});
token = loginRes.body.token;
});
it('should retrieve a list of users with valid token', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('name', 'Charlie');
expect(res.body[0]).toHaveProperty('email', 'charlie@example.com');
});
it('should not retrieve users without token', async () => {
const res = await request(app).get('/users');
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Access token missing');
});
});
});
Explanation:
• Token Acquisition: Logs in using a test user to obtain a JWT for accessing protected routes.
• Protected Route Testing: Ensures that the /users endpoint is accessible with a valid token and restricted otherwise.
c. user-service/tests/userProfile.test.js
javascript
Copy code
// user-service/tests/userProfile.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { User, schemas } = require('common');
const UserProfile = require('../models/UserProfile');
const bcrypt = require('bcrypt');
describe('User Profile Service API Endpoints', () => {
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/user_profile_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user
const hashedPassword = await bcrypt.hash('userpassword', 10);
const testUser = new User({
name: 'Dave',
email: 'dave@example.com',
password: hashedPassword,
});
await testUser.save();
testUserId = testUser._id.toString();
// Login to get the token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'dave@example.com',
password: 'userpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await UserProfile.deleteMany({});
await User.deleteMany({});
await mongoose.connection.close();
});
describe('POST /users', () => {
it('should create a new user profile', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId,
address: '789 Test Avenue',
phone: '555-1234',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUserId);
expect(res.body).toHaveProperty('address', '789 Test Avenue');
expect(res.body).toHaveProperty('phone', '555-1234');
});
it('should not create a user profile with existing userId', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId,
address: '789 Test Avenue',
phone: '555-1234',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User profile already exists');
});
it('should fail to create a user profile without userId', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
address: 'No User ID Street',
phone: '555-0000',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User ID is required');
});
});
describe('GET /users', () => {
it('should retrieve a list of user profiles', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('address', '789 Test Avenue');
expect(res.body[0].userId).toHaveProperty('name', 'Dave');
});
});
describe('GET /users/:id', () => {
it('should retrieve a user profile by user ID', async () => {
const res = await request(app)
.get(`/users/${testUserId}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('userId');
expect(res.body.userId).toHaveProperty('name', 'Dave');
expect(res.body).toHaveProperty('address', '789 Test Avenue');
});
it('should not retrieve a non-existent user profile', async () => {
const res = await request(app)
.get('/users/60d5f9f9b1e8e1d4c8e4f8d')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'User profile not found');
});
});
});
Explanation:
• Token Acquisition: Logs in using a test user to obtain a JWT for accessing protected routes.
• Profile Creation Tests: Validates successful profile creation, prevents duplicates, and ensures required fields are present.
• Profile Retrieval Tests: Checks fetching all profiles and specific profiles by userId.
d. common/__tests__/EventSchemaValidation.test.js
javascript
Copy code
// common/__tests__/EventSchemaValidation.test.js
const { Validator } = require('jsonschema');
const { schemas } = require('../index');
const validator = new Validator();
describe('Event Schema Validation', () => {
it('should validate user_registered event successfully', () => {
const validEvent = {
event: 'user_registered',
data: {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Eve',
email: 'eve@example.com',
},
};
const validation = validator.validate(validEvent, schemas.userRegistered);
expect(validation.valid).toBe(true);
});
it('should fail validation for user_registered event with missing fields', () => {
const invalidEvent = {
event: 'user_registered',
data: {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
// Missing name and email
},
};
const validation = validator.validate(invalidEvent, schemas.userRegistered);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
it('should validate product_created event successfully', () => {
const validEvent = {
event: 'product_created',
data: {
id: '60d5fb0ab1e8e1d4c8e4f8c7',
name: 'Hat',
price: 19.99,
},
};
const validation = validator.validate(validEvent, schemas.productCreated);
expect(validation.valid).toBe(true);
});
it('should fail validation for product_created event with incorrect data types', () => {
const invalidEvent = {
event: 'product_created',
data: {
id: '60d5fb0ab1e8e1d4c8e4f8c7',
name: 'Hat',
price: '19.99', // Should be a number
},
};
const validation = validator.validate(invalidEvent, schemas.productCreated);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
});
Explanation:
• Schema Validation Tests: Ensures that events conform to their defined JSON Schemas.
• Positive and Negative Cases: Tests both valid and invalid event structures to verify the robustness of schema validations.
________________________________________
4.3. Writing Integration Tests
Integration Tests evaluate the interactions between different components or services, ensuring they work together as expected.
4.3.1. Using In-Memory Databases
To avoid affecting the actual databases during testing, use MongoDB Memory Server to run an in-memory MongoDB instance.
Installing MongoDB Memory Server
bash
Copy code
# Navigate to each microservice and the common library to install the package
cd auth-service
npm install --save-dev mongodb-memory-server
cd ../user-service
npm install --save-dev mongodb-memory-server
cd ../product-service
npm install --save-dev mongodb-memory-server
cd ../notification-service
npm install --save-dev mongodb-memory-server
cd ../common
npm install --save-dev mongodb-memory-server
Configuring Tests to Use MongoDB Memory Server
Example: auth-service/tests/auth.test.js (Modified for In-Memory DB)
javascript
Copy code
// auth-service/tests/auth.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { User } = require('common');
const bcrypt = require('bcrypt');
let mongoServer;
describe('Authentication Service API Endpoints', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
// ... (Rest of the tests remain the same)
});
Explanation:
• MongoMemoryServer: Spins up an in-memory MongoDB instance for testing purposes.
• Isolation: Ensures tests run in isolation without affecting the development or production databases.
Repeat the Setup for Other Microservices
Ensure that each microservice's tests connect to the in-memory MongoDB instance instead of the actual database.
Example: user-service/tests/user.test.js
javascript
Copy code
// user-service/tests/user.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { User } = require('common');
const bcrypt = require('bcrypt');
let mongoServer;
describe('User Service API Endpoints', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user
const hashedPassword = await bcrypt.hash('charliespassword', 10);
const testUser = new User({
name: 'Charlie',
email: 'charlie@example.com',
password: hashedPassword,
});
await testUser.save();
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
// ... (Rest of the tests remain the same)
});
Note: Adjust environment variables or configurations as needed to point to the in-memory database during testing.
________________________________________
4.4. Running Tests
4.4.1. Adding Test Scripts in package.json
Ensure each microservice and the common library have appropriate test scripts.
Example: auth-service/package.json
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"nodemon": "^3.0.1",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js"
]
}
}
Explanation:
• test: Runs Jest with coverage reports.
• collectCoverageFrom: Specifies which files to include in coverage reports.
4.4.2. Executing Tests
Navigate to each microservice and the common library to run tests.
bash
Copy code
# From the root directory
cd auth-service
npm test
cd ../user-service
npm test
cd ../product-service
npm test
cd ../notification-service
npm test
cd ../common
npm test
Example Output:
sql
Copy code
PASS auth-service/tests/auth.test.js
Authentication Service API Endpoints
POST /auth/register
✓ should register a new user (XXX ms)
✓ should not register a user with existing email (XXX ms)
✓ should fail registration with missing fields (XXX ms)
POST /auth/login
✓ should login an existing user (XXX ms)
✓ should not login with incorrect password (XXX ms)
✓ should not login a non-existent user (XXX ms)
GET /users
✓ should retrieve a list of users with valid token (XXX ms)
✓ should not retrieve users without token (XXX ms)
✓ should not retrieve users with invalid token (XXX ms)
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: X.XXX s, estimated Y s
Ran all test suites.
Coverage Report:
After running tests, Jest generates a coverage report indicating the percentage of code tested.
Example Coverage Report:
bash
Copy code
-------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------|---------|----------|---------|---------|-------------------
All files | 85.71 | 75% | 80% | 85.71 |
common/ | 85.71 | 75% | 80% | 85.71 |
models/User.js | 100% | 100%| 100% | 100% |
models/Product.js| 100% | 100%| 100% | 100% |
index.js | 66.67 | 50%| 50% | 66.67 | 8-9
schemas/ | 50% | 50% | 0% | 50% | 1-3
-------------------|---------|----------|---------|---------|-------------------
auth-service/ | 90.48 | 80% | 85.71 | 90.48 |
controllers/authController.js | 90.48 | 80% | 85.71 | 90.48 | 25
models/User.js | 100% | 100%| 100% | 100% |
routes/authRoutes.js | 100% | 100%| 100% | 100% |
middleware/authenticate.js | 100% | 100%| 100% | 100% |
services/rabbitmqService.js | 80% | 100%| 100%| 80% | 15-16
-------------------|---------|----------|---------|---------|-------------------
Interpretation:
• % Stmts: Percentage of statements executed.
• % Branch: Percentage of branches (e.g., if-else) executed.
• % Funcs: Percentage of functions executed.
• % Lines: Percentage of lines executed.
• Uncovered Line #s: Specific lines not covered by tests.
________________________________________
4.5. End-to-End (E2E) Testing (Optional for Future Stages)
While comprehensive unit and integration tests are essential, E2E tests simulate real user interactions, testing the entire workflow from start to finish.
Tools:
• Cypress – A powerful E2E testing framework.
• Puppeteer – A Node library for controlling headless Chrome.
Note: Given the complexity, E2E testing can be introduced in a later stage to keep the learning curve manageable.
________________________________________
4.6. Continuous Integration (Optional for Future Stages)
Implementing CI/CD pipelines ensures that tests run automatically on code commits, maintaining code quality over time.
Tools:
• GitHub Actions
• Jenkins
• Travis CI
Note: Setup CI/CD in future stages after establishing a solid testing foundation.
________________________________________
5. Complete Code and Configuration Files
5.1. Common Library (common/)
common/package.json
json
Copy code
{
"name": "common",
"version": "1.0.0",
"description": "Shared models and schemas for iBranDIDit.com microservices",
"main": "index.js",
"scripts": {
"test": "jest --coverage"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"jsonschema": "^1.4.0",
"mongoose": "^7.0.3"
},
"devDependencies": {
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"models/**/*.js",
"schemas/**/*.json",
"index.js"
]
}
}
common/models/User.js
(Already provided in Stage 6)
common/models/Product.js
(Already provided in Stage 6)
common/schemas/userRegistered.json
(Already provided in Stage 6)
common/schemas/productCreated.json
(Already provided in Stage 6)
common/index.js
(Already provided in Stage 6)
common/__tests__/User.test.js
(Already provided in section 4.2.1.a)
common/__tests__/Product.test.js
(Already provided in section 4.2.1.b)
common/__tests__/EventSchemaValidation.test.js
(Already provided in section 4.2.4)
________________________________________
5.2. Authentication Service (auth-service/)
auth-service/package.json
(Already provided in section 4.4.1.a)
auth-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/auth_test
Note: MONGODB_URI_TEST points to the in-memory database during testing.
auth-service/models/User.js
(Already updated in section 4.3.2.a)
javascript
Copy code
// auth-service/models/User.js
const { User } = require('common');
module.exports = User;
auth-service/controllers/authController.js
(Already provided in section 4.3.2.b)
auth-service/routes/authRoutes.js
(Already provided in section 4.3.2.b)
auth-service/middleware/authenticate.js
(Assuming it's already present from previous stages. If not, create it as follows.)
javascript
Copy code
// auth-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
module.exports = function (req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ message: 'Access token missing' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Invalid or expired token' });
}
req.user = user;
// Add user info to headers for downstream services
req.headers['x-user-id'] = user.id;
req.headers['x-user-email'] = user.email;
next();
});
};
auth-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure compatibility with shared models.)
auth-service/tests/auth.test.js
(Already provided in section 4.2.1.a and modified in 4.3.3.a)
auth-service/tests/user.test.js
(Already provided in section 4.3.3.c)
auth-service/jest.config.js
(Already provided in section 4.1.2)
javascript
Copy code
// auth-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
auth-service/jest.setup.js
(Optional: If additional setup is required)
javascript
Copy code
// auth-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
auth-service/swagger.js
(Already provided in Stage 6)
auth-service/Dockerfile
(Already provided in Stage 5, ensure compatibility)
dockerfile
Copy code
# auth-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5003
CMD [ "npm", "start" ]
________________________________________
5.3. User Service (user-service/)
user-service/package.json
(Already provided in section 4.4.1.a)
user-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5001
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/userdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/user_profile_test
user-service/models/UserProfile.js
(Already provided in section 4.3.3.i)
user-service/controllers/userController.js
(Already provided in section 4.3.3.b)
user-service/routes/userRoutes.js
(Already provided in section 4.3.3.c)
user-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure compatibility with shared models if needed.)
user-service/tests/user.test.js
(Already provided in section 4.3.3.d)
user-service/tests/userProfile.test.js
(Already provided in section 4.3.3.d)
user-service/jest.config.js
javascript
Copy code
// user-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
user-service/jest.setup.js
(Optional: If additional setup is required)
javascript
Copy code
// user-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
user-service/swagger.js
(Already provided in Stage 6)
user-service/Dockerfile
(Already provided in section 4.3.3.f)
dockerfile
Copy code
# user-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5001
CMD [ "npm", "start" ]
________________________________________
5.4. Product Service (product-service/)
product-service/package.json
(Already provided in section 4.4.1.a)
product-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5002
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/product_test
product-service/models/Product.js
(Already updated in section 4.3.3.a)
product-service/controllers/productController.js
(Already provided in section 4.3.3.b)
product-service/routes/productRoutes.js
(Already provided in section 4.3.3.c)
product-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure compatibility with shared models if needed.)
product-service/tests/product.test.js
(Already provided in section 4.3.3.d)
product-service/tests/productIntegration.test.js
javascript
Copy code
// product-service/tests/productIntegration.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { Product } = require('common');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
describe('Product Service Integration Tests', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
it('should create a new product and publish event', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Test Product Integration',
price: 59.99,
description: 'Integration test product',
inventory: 30,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('name', 'Test Product Integration');
expect(res.body).toHaveProperty('price', 59.99);
expect(res.body).toHaveProperty('description', 'Integration test product');
expect(res.body).toHaveProperty('inventory', 30);
// Optionally, verify that the event was published to RabbitMQ
// This requires mocking or spying on the rabbitmqService.publishEvent method
// For simplicity, this step can be skipped or handled with more advanced setups
});
it('should retrieve all products', async () => {
const res = await request(app).get('/products');
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBe(1);
expect(res.body[0]).toHaveProperty('name', 'Test Product Integration');
});
});
Explanation:
• Integration Test: Checks the entire flow of creating a product and ensuring it's retrievable.
• Event Verification: To fully test event publishing, consider mocking rabbitmqService.publishEvent using Jest spies or mocks. This ensures that the event is attempted to be published without relying on an actual RabbitMQ instance.
Note: For more advanced integration testing involving message queues, additional setup or specialized testing tools may be required.
product-service/jest.config.js
(Already provided in section 4.1.2)
javascript
Copy code
// product-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
product-service/jest.setup.js
(Optional: If additional setup is required)
javascript
Copy code
// product-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
product-service/swagger.js
(Already provided in Stage 6)
product-service/Dockerfile
(Already provided in section 4.3.3.f)
dockerfile
Copy code
# product-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5002
CMD [ "npm", "start" ]
________________________________________
5.5. Notification Service (notification-service/)
notification-service/package.json
(Already provided in section 4.4.1.a)
notification-service/.env
(Already provided in Stage 5, ensure consistency)
env
Copy code
PORT=5004
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/notificationdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/notification_test
notification-service/index.js
(Already provided in Stage 5, ensure compatibility with common)
notification-service/controllers/notificationController.js
(Optional: Implement if exposing APIs for notifications.)
notification-service/routes/notificationRoutes.js
(Optional: Implement if exposing APIs for notifications.)
notification-service/services/rabbitmqService.js
(Already provided in Stage 4. Ensure compatibility with shared models if needed.)
notification-service/tests/notification.test.js
(Already provided in Stage 4)
Example Test:
javascript
Copy code
// notification-service/tests/notification.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
describe('Notification Service API Endpoints', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB if necessary
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
it('should return welcome message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toEqual(200);
expect(res.text).toBe('Welcome to Notification Service API');
});
});
Explanation:
• Basic Route Testing: Ensures that the root endpoint responds correctly.
• Further Tests: Depending on implementation, test event consumption and notification dispatching. This might involve mocking RabbitMQ or using test queues.
notification-service/jest.config.js
(Already provided in section 4.1.2)
javascript
Copy code
// notification-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
notification-service/jest.setup.js
(Optional: If additional setup is required)
javascript
Copy code
// notification-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
notification-service/swagger.js
(Already provided in Stage 6)
notification-service/Dockerfile
(Already provided in section 4.3.3.f)
dockerfile
Copy code
# notification-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 5004
CMD [ "npm", "start" ]
________________________________________
5.6. API Gateway (api-gateway/)
Note: The API Gateway primarily handles routing and authentication. Integration tests can ensure that routing works as expected.
api-gateway/package.json
(Already provided in section 4.4.1.a)
api-gateway/config/gateway.config.yml
(Already provided in Stage 6, ensure JWT_SECRET matches auth-service.)
api-gateway/policies/jwt-auth/jwt-auth.js
(Already provided in Stage 4, ensure compatibility with testing.)
api-gateway/Dockerfile
(Already provided in section 4.3.3.f)
dockerfile
Copy code
# api-gateway/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "npm", "start" ]
api-gateway/__tests__/gateway.test.js
javascript
Copy code
// api-gateway/__tests__/gateway.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { User } = require('common');
const bcrypt = require('bcrypt');
let mongoServer;
describe('API Gateway Integration Tests', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB if necessary
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user
const hashedPassword = await bcrypt.hash('gatewaypassword', 10);
const testUser = new User({
name: 'Gateway User',
email: 'gateway@example.com',
password: hashedPassword,
});
await testUser.save();
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
it('should route to auth-service and register a new user', async () => {
const res = await request(app)
.post('/auth/register')
.send({
name: 'Test Gateway User',
email: 'testgateway@example.com',
password: 'testpassword',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'User registered successfully');
expect(res.body).toHaveProperty('user');
expect(res.body.user).toHaveProperty('name', 'Test Gateway User');
});
it('should route to product-service and create a new product', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Gateway Test Product',
price: 29.99,
description: 'Product created via API Gateway',
inventory: 20,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('name', 'Gateway Test Product');
});
});
Explanation:
• Routing Tests: Verifies that the API Gateway correctly routes requests to the respective services.
• User and Product Creation: Tests registration and product creation through the gateway, ensuring end-to-end functionality.
Note: For full integration, ensure that the microservices are accessible to the API Gateway during testing. This might require starting all services or mocking them.
________________________________________
6. Documentation and Usage
6.1. Running Tests Locally
1. Ensure All Services Are Running:
o Since integration tests may involve multiple services, ensure that dependent services (like Auth Service) are running.
o Alternatively, use Docker Compose to spin up necessary services for testing.
2. Execute Tests:
o From the Root Directory:
bash
Copy code
npm install
o Run Tests for Each Microservice and the common Library:
bash
Copy code
cd common
npm test
cd ../auth-service
npm test
cd ../user-service
npm test
cd ../product-service
npm test
cd ../notification-service
npm test
cd ../api-gateway
npm test
3. View Coverage Reports:
o After running tests, coverage reports are generated in the coverage/ directory of each package.
o Open coverage/lcov-report/index.html in a browser to view detailed coverage.
6.2. Understanding Test Results
• PASS: Indicates that the test case met all expectations.
• FAIL: Indicates discrepancies between expected and actual outcomes.
• Coverage Metrics:
o Statements: Percentage of executable statements tested.
o Branches: Percentage of control flow branches tested.
o Functions: Percentage of functions invoked during tests.
o Lines: Percentage of lines executed.
Aim for high coverage, focusing on critical paths and functionalities.
6.3. Writing New Tests
1. Identify the Component to Test:
o Models: Validate schema definitions and data constraints.
o Controllers: Ensure correct handling of requests and responses.
o Services: Test business logic and external integrations.
2. Create Test Files:
o Follow the naming convention component.test.js (e.g., userController.test.js).
3. Implement Test Cases:
o Positive Cases: Valid inputs leading to expected outcomes.
o Negative Cases: Invalid inputs or error scenarios.
4. Run Tests Frequently:
o Use npm test to run tests after writing new ones to ensure they pass.
6.4. Mocking External Dependencies
• RabbitMQ: Mock rabbitmqService.publishEvent to prevent actual message publishing during tests.
Example: Mocking in Jest
javascript
Copy code
// auth-service/tests/auth.test.js
jest.mock('../services/rabbitmqService', () => ({
publishEvent: jest.fn().mockResolvedValue(true),
}));
const rabbitmqService = require('../services/rabbitmqService');
// Now, in your tests, you can assert if publishEvent was called
expect(rabbitmqService.publishEvent).toHaveBeenCalledWith('user_events', expect.any(Object));
Explanation:
• jest.mock: Replaces the actual implementation with a mock function.
• mockResolvedValue: Ensures the mock returns a resolved promise, simulating successful event publishing.
________________________________________
7. Troubleshooting Tips
1. Tests Failing Due to Database Connection Issues:
o Ensure In-Memory Database Is Running:
Verify that MongoMemoryServer initializes correctly in your test setup.
o Check Environment Variables:
Ensure MONGODB_URI_TEST points to the correct in-memory database URI.
o Review Service Connections:
Confirm that microservices connect to the in-memory database during testing.
2. Mocking Failures:
o Incorrect Mock Paths:
Ensure the correct relative paths are used in jest.mock.
o Mock Not Reset Between Tests:
Use jest.resetAllMocks() in beforeEach or afterEach hooks if necessary.
3. Coverage Reports Inaccurate:
o Incorrect collectCoverageFrom Paths:
Verify that the paths in collectCoverageFrom accurately reflect the file structure.
o Exclude Test Files:
Ensure test files are not included in coverage reports by specifying only source files.
4. Timeouts During Tests:
o Increase Jest Timeout:
Modify jest.setTimeout(30000); in jest.setup.js to accommodate longer-running tests.
o Optimize Test Performance:
Ensure tests are efficient and avoid unnecessary delays.
5. Uncaught Exceptions or Errors:
o Review Test Setup:
Ensure all necessary setup steps (e.g., connecting to databases) are completed before tests run.
o Handle Promises Correctly:
Use async/await or return promises to ensure Jest waits for asynchronous operations.
________________________________________
8. Next Steps
With Stage 7: Comprehensive Testing completed, your microservices architecture now boasts a robust testing framework ensuring each component functions correctly both in isolation and in collaboration with others. This foundation is crucial for building a reliable and maintainable eCommerce platform.
Proceed to Stage 8: Implementing User Roles and Permissions
In the next stage, we'll introduce User Roles (e.g., Admin, Customer) and implement Role-Based Access Control (RBAC) to secure your platform effectively. This enhancement will ensure that users have appropriate access rights based on their roles, bolstering the security and integrity of your application.
________________________________________
9. Additional Resources
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• MongoDB Memory Server: https://github.com/nodkz/mongodb-memory-server
• JSON Schema Documentation: https://json-schema.org/
• Mongoose Documentation: https://mongoosejs.com/docs/
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• Jest Mock Functions: https://jestjs.io/docs/mock-functions
• Docker Compose Documentation: https://docs.docker.com/compose/
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• Swagger Documentation: https://swagger.io/docs/
• Jest Coverage Reports: https://jestjs.io/docs/configuration#collectcoveragefrom-array
________________________________________
Disclaimer: This guide provides a foundational approach to comprehensive testing within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and advanced testing strategies may be necessary, such as integrating CI/CD pipelines, utilizing more sophisticated mocking techniques, or implementing additional test types like performance or security tests.
________________________________________
By following this comprehensive guide for Stage 7: Comprehensive Testing, you've fortified your microservices architecture with a robust testing suite, ensuring that your eCommerce platform is reliable, maintainable, and scalable. This setup not only enhances code quality but also instills confidence in deploying updates and new features.
Stage 8: Version Control & Documentation is crucial for maintaining code integrity, facilitating collaboration, and ensuring that your project remains manageable as it grows. This stage will guide you through setting up a robust version control system using Git, establishing best practices for version management, and creating comprehensive documentation to aid both current and future developers.
________________________________________
Stage 8: Version Control & Documentation
________________________________________
1. Overview
Objective:
• Implement Version Control: Utilize Git to track changes, manage code versions, and collaborate efficiently across all microservices.
• Establish Documentation Standards: Create clear and comprehensive documentation for the entire project and each microservice to facilitate understanding and maintenance.
• Ensure Easy Integration: Set up processes that allow developers to contribute seamlessly, maintain code quality, and understand the project structure.
Benefits:
• Code History Tracking: Maintain a complete history of changes, enabling rollback to previous states if necessary.
• Collaboration Facilitation: Allow multiple developers to work on the project simultaneously without conflicts.
• Enhanced Code Quality: Encourage best practices through structured commits and code reviews.
• Knowledge Sharing: Provide clear documentation that helps new developers onboard quickly and existing developers understand the system intricacies.
________________________________________
2. Technology Stack
• Version Control: Git – A distributed version control system.
• Hosting Platform: GitHub, GitLab, or Bitbucket – Platforms for hosting Git repositories with additional features like issue tracking and CI/CD integrations.
• Documentation Tools:
o Markdown: For writing README files and documentation.
o JSDoc: For generating API documentation from code comments.
o Swagger (OpenAPI): For detailed API documentation (already implemented in previous stages).
o MkDocs or Docusaurus: For creating a centralized documentation website (optional for future enhancements).
• Continuous Integration (Optional for Future Stages): GitHub Actions, GitLab CI/CD, or CircleCI – For automating testing and deployment processes.
________________________________________
3. Project Structure Review
Before setting up version control and documentation, ensure that your project structure aligns with best practices. Given that you've implemented NPM Workspaces in Stage 6, your project follows a monorepo approach, which centralizes all microservices and the common library within a single repository.
Monorepo Structure:
lua
Copy code
ibrandidit-microservices/
├── common/
│ ├── models/
│ ├── schemas/
│ ├── __tests__/
│ ├── package.json
│ └── README.md
├── api-gateway/
│ ├── config/
│ ├── policies/
│ ├── Dockerfile
│ ├── package.json
│ ├── __tests__/
│ └── README.md
├── auth-service/
│ ├── controllers/
│ ├── middleware/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── tests/
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── user-service/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── tests/
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── product-service/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── tests/
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── notification-service/
│ ├── controllers/
│ ├── routes/
│ ├── services/
│ ├── tests/
│ ├── .env
│ ├── index.js
│ ├── jest.config.js
│ ├── swagger.js
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml
├── package.json
└── README.md
Explanation:
• common/: Shared resources like models and schemas.
• Each Microservice (api-gateway/, auth-service/, etc.): Contains its own controllers, models, routes, services, tests, and configurations.
• Root Directory (ibrandidit-microservices/): Contains Docker configurations, root package.json, and a central README.md.
________________________________________
4. Implementation Steps
4.1. Setting Up Version Control with Git
4.1.1. Initializing Git Repository
1. Navigate to Root Directory:
Open your terminal and navigate to the root directory of your project.
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Initialize Git:
Initialize a new Git repository.
bash
Copy code
git init
3. Add Remote Repository:
Create a repository on your chosen platform (GitHub, GitLab, Bitbucket) and add it as a remote.
bash
Copy code
git remote add origin https://github.com/yourusername/ibrandidit-microservices.git
Note: Replace https://github.com/yourusername/ibrandidit-microservices.git with your actual repository URL.
4.1.2. Configuring .gitignore Files
Ensure that sensitive and unnecessary files are excluded from version control. Create a .gitignore file in the root directory and individual microservices as needed.
1. Root .gitignore:
Create a .gitignore file in the root directory to exclude global files.
bash
Copy code
touch .gitignore
Content:
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
docker-compose.override.yml
# OS files
.DS_Store
Thumbs.db
2. Microservices .gitignore:
For each microservice and the common library, create a .gitignore file to exclude service-specific files.
Example: auth-service/.gitignore
bash
Copy code
cd auth-service
touch .gitignore
Content:
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
Dockerfile
# OS files
.DS_Store
Thumbs.db
Repeat this process for:
o common/.gitignore
o user-service/.gitignore
o product-service/.gitignore
o notification-service/.gitignore
o api-gateway/.gitignore
4.1.3. Committing Initial Code
1. Add All Files:
bash
Copy code
git add .
2. Commit with Descriptive Message:
bash
Copy code
git commit -m "Initial commit: Setup project structure with microservices and common library"
4.1.4. Pushing to Remote Repository
bash
Copy code
git push -u origin master
Note: Depending on your Git provider's default branch naming, you might need to use main instead of master.
bash
Copy code
git push -u origin main
________________________________________
4.2. Establishing Branching Strategy
Implementing a branching strategy ensures organized development and minimizes conflicts.
4.2.1. Choosing a Branching Model
For beginners, the GitHub Flow is simple and effective. It involves:
• main Branch: Always production-ready.
• Feature Branches: Created for new features or bug fixes.
• Pull Requests (PRs): Reviewed and merged into main.
4.2.2. Creating a Feature Branch
1. Create and Switch to a New Branch:
bash
Copy code
git checkout -b feature/setup-version-control
2. Make Changes (if any), Commit, and Push:
bash
Copy code
# Make necessary changes or simply push the branch
git add .
git commit -m "Setup version control and initial documentation"
git push -u origin feature/setup-version-control
3. Create a Pull Request:
o Navigate to your repository on GitHub (or chosen platform).
o You'll see a prompt to create a Pull Request (PR) for the recently pushed branch.
o Click "Compare & pull request".
o Provide a descriptive title and description.
o Assign reviewers if collaborating with others.
o Click "Create pull request".
4. Merging the PR:
o Once reviewed and approved, click "Merge pull request".
o Optionally, delete the feature branch after merging.
Repeat this process for all future features and bug fixes.
________________________________________
4.3. Creating Comprehensive Documentation
Documentation is vital for understanding, maintaining, and scaling your project. It encompasses general project information, individual microservice details, API documentation, and contribution guidelines.
4.3.1. Root README.md
Create a central README.md in the root directory to provide an overview of the project.
1. Create or Open README.md:
bash
Copy code
cd path/to/ibrandidit-microservices/
touch README.md
2. Content Structure:
markdown
Copy code
# iBranDIDit.com Microservices Architecture
Welcome to the iBranDIDit.com eCommerce platform, built using a microservices architecture. This repository contains all the services that make up the platform, including the API Gateway, Authentication Service, User Service, Product Service, Notification Service, and a Common library for shared resources.
## Table of Contents
- [Project Structure](#project-structure)
- [Getting Started](#getting-started)
- [Microservices](#microservices)
- [Common Library](#common-library)
- [API Gateway](#api-gateway)
- [Authentication Service](#authentication-service)
- [User Service](#user-service)
- [Product Service](#product-service)
- [Notification Service](#notification-service)
- [Version Control](#version-control)
- [Testing](#testing)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
## Project Structure
ibrandidit-microservices/ ├── common/ ├── api-gateway/ ├── auth-service/ ├── user-service/ ├── product-service/ ├── notification-service/ ├── docker-compose.yml ├── package.json └── README.md
markdown
Copy code
## Getting Started
Follow these instructions to set up the project locally.
### Prerequisites
- [Docker](https://www.docker.com/get-started)
- [Docker Compose](https://docs.docker.com/compose/install/)
- [Node.js](https://nodejs.org/) (if running services outside Docker)
### Installation
1. **Clone the Repository:**
```bash
git clone https://github.com/yourusername/ibrandidit-microservices.git
cd ibrandidit-microservices/
2. Set Up Environment Variables:
Each microservice contains a .env file. Ensure that all necessary environment variables are set. Refer to the individual service's documentation for details.
3. Install Dependencies:
bash
Copy code
npm install
4. Start Services with Docker Compose:
bash
Copy code
docker-compose up --build
Note: Ensure that Docker is running on your machine.
Microservices
Common Library
o Path: common/
o Description: Contains shared models and schemas used across all microservices.
o Documentation: common/README.md
API Gateway
o Path: api-gateway/
o Description: Handles routing, authentication, and acts as the entry point for all client requests.
o Documentation: api-gateway/README.md
Authentication Service
o Path: auth-service/
o Description: Manages user authentication, registration, and JWT token issuance.
o Documentation: auth-service/README.md
User Service
o Path: user-service/
o Description: Handles user profiles and related operations.
o Documentation: user-service/README.md
Product Service
o Path: product-service/
o Description: Manages product catalog, inventory, and related functionalities.
o Documentation: product-service/README.md
Notification Service
o Path: notification-service/
o Description: Handles sending notifications based on events from other services.
o Documentation: notification-service/README.md
Version Control
We utilize Git for version control, following the GitHub Flow for streamlined development.
Testing
Comprehensive testing is implemented using Jest and Supertest. Refer to each microservice's documentation for testing instructions.
Documentation
o API Documentation: Accessible via Swagger UI for each microservice (e.g., http://localhost:5003/api-docs for Authentication Service).
o Code Documentation: Generated using JSDoc. Run npm run swagger-autogen within each service to update documentation.
Contributing
Contributions are welcome! Please follow these steps:
5. Fork the Repository.
6. Create a Feature Branch:
bash
Copy code
git checkout -b feature/your-feature-name
7. Commit Your Changes:
bash
Copy code
git commit -m "Add some feature"
8. Push to the Branch:
bash
Copy code
git push origin feature/your-feature-name
9. Open a Pull Request.
License
This project is licensed under the MIT License.
markdown
Copy code
**Explanation:**
- **Overview:** Brief introduction to the project.
- **Table of Contents:** Easy navigation through the documentation.
- **Sections:** Detailed descriptions of each component, setup instructions, version control, testing, and contribution guidelines.
- **Links:** Direct references to individual microservice documentation for in-depth details.
4.3.2. Microservice README.md Files
Each microservice should have its own README.md detailing its purpose, setup instructions, and usage.
Example: auth-service/README.md
markdown
Copy code
# Authentication Service
## Overview
The Authentication Service handles user registration, login, and JWT token issuance. It interacts with the **Common** library for shared models and schemas and communicates with other services via RabbitMQ.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Contributing](#contributing)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [Docker](https://www.docker.com/get-started) (optional, if running via Docker)
- [Docker Compose](https://docs.docker.com/compose/install/) (optional)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd auth-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the auth-service/ directory based on the provided .env.example.
env
Copy code
PORT=5003
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRES_IN=1h
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/auth_test
Descriptions:
• PORT: Port on which the service runs.
• JWT_SECRET: Secret key for signing JWT tokens.
• JWT_EXPIRES_IN: Token expiration duration.
• RABBITMQ_URL: Connection string for RabbitMQ.
• MONGODB_URI: Connection string for the production MongoDB database.
• MONGODB_URI_TEST: Connection string for the test MongoDB database.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
1. Start the Service:
bash
Copy code
docker-compose up --build auth-service
Option 2: Running Locally
1. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
2. Start the Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
• URL: http://localhost:5003/api-docs
Testing
Run tests using Jest:
bash
Copy code
npm test
Coverage Reports:
After running tests, view coverage reports in the coverage/ directory.
Contributing
Contributions are welcome! Please refer to the root README for guidelines.
________________________________________
markdown
Copy code
**Explanation:**
- **Overview:** Brief description of the service's purpose.
- **Table of Contents:** Navigational aid.
- **Sections:**
- **Prerequisites:** Software requirements.
- **Installation:** Steps to install dependencies.
- **Environment Variables:** Detailed explanation of each variable.
- **Running the Service:** Instructions for both Docker and local setups.
- **API Documentation:** Link to Swagger UI.
- **Testing:** How to run tests and view coverage.
- **Contributing:** Reference to the main contributing guidelines.
**Repeat this structure for each microservice and the `common` library, adjusting sections as necessary based on each component's specifics.**
#### **4.3.3. Implementing Code Documentation with JSDoc**
**JSDoc** allows you to generate HTML documentation from annotated JavaScript code. It is particularly useful for documenting APIs, functions, classes, and modules.
#### **4.3.3.1. Installing JSDoc**
1. **Navigate to Each Microservice and the `common` Library:**
```bash
cd auth-service
2. Install JSDoc as a Development Dependency:
bash
Copy code
npm install --save-dev jsdoc
4.3.3.2. Configuring JSDoc
1. Create a jsdoc.json Configuration File:
bash
Copy code
touch jsdoc.json
2. Sample jsdoc.json:
json
Copy code
{
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": true,
"monospaceLinks": true
}
}
3. Update package.json Scripts:
Add a script to generate documentation.
json
Copy code
{
"scripts": {
"docs": "jsdoc -c jsdoc.json"
}
}
4.3.3.3. Annotating Code with JSDoc Comments
Add JSDoc comments to your code to describe functions, classes, and modules.
Example: auth-service/controllers/authController.js
javascript
Copy code
// auth-service/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const rabbitmqService = require('../services/rabbitmqService');
const { User, schemas } = require('common');
const { Validator } = require('jsonschema');
const validator = new Validator();
/**
* Registers a new user.
*
* @param {Object} req - Express request object.
* @param {Object} res - Express response object.
* @returns {Object} JSON response with registration status.
*/
exports.register = async (req, res) => {
const { name, email, password } = req.body;
// Basic validation
if (!name || !email || !password) {
return res.status(400).json({ message: 'Name, email, and password are required' });
}
try {
// Check if user already exists
const existingUser = await User.findOne({ email: email.toLowerCase() });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create new user
const newUser = new User({
name,
email: email.toLowerCase(),
password: hashedPassword,
});
await newUser.save();
// Validate event against schema
const event = {
event: 'user_registered',
data: {
id: newUser._id.toString(),
name: newUser.name,
email: newUser.email,
},
};
const validation = validator.validate(event, schemas.userRegistered);
if (!validation.valid) {
console.error('Event validation failed:', validation.errors);
return res.status(500).json({ message: 'Invalid event structure' });
}
// Publish event to RabbitMQ
await rabbitmqService.publishEvent('user_events', event);
res.status(201).json({ message: 'User registered successfully', user: { id: newUser._id, name, email: newUser.email } });
} catch (error) {
console.error('Error during registration:', error);
res.status(500).json({ message: 'Server error during registration' });
}
};
Explanation:
• @param Tags: Describe the parameters the function accepts.
• @returns Tag: Describes what the function returns.
• Function Description: Brief overview of what the function does.
Repeat this process for all controllers, models, and significant functions across all microservices and the common library.
4.3.3.4. Generating Documentation
1. Run the Documentation Generator:
bash
Copy code
npm run docs
2. Access the Generated Documentation:
o Navigate to the docs/ directory within each microservice or the common library.
o Open index.html in a web browser to view the generated documentation.
Note: Integrate documentation generation into your development workflow to keep it up-to-date with code changes.
________________________________________
4.4. Establishing Contribution Guidelines
Clear contribution guidelines help maintain code quality and streamline the onboarding process for new contributors.
4.4.1. Creating CONTRIBUTING.md
1. Create the File:
bash
Copy code
touch CONTRIBUTING.md
2. Sample CONTRIBUTING.md:
markdown
Copy code
# Contributing to iBranDIDit.com Microservices
Thank you for your interest in contributing to iBranDIDit.com! Your contributions are greatly appreciated.
## How to Contribute
1. **Fork the Repository:**
Click the "Fork" button at the top-right corner of the repository page.
2. **Clone Your Fork:**
```bash
git clone https://github.com/yourusername/ibrandidit-microservices.git
cd ibrandidit-microservices/
3. Create a Branch:
bash
Copy code
git checkout -b feature/your-feature-name
4. Make Your Changes:
Implement your feature or bug fix. Ensure that you follow the coding standards and conventions used in the project.
5. Commit Your Changes:
bash
Copy code
git add .
git commit -m "Add feature: your feature description"
6. Push to Your Fork:
bash
Copy code
git push origin feature/your-feature-name
7. Open a Pull Request:
Navigate to the original repository and click on "Compare & pull request". Provide a clear description of your changes.
Coding Standards
o Language: JavaScript (ES6+)
o Linting: Follow the existing linting rules. (Implement ESLint in future stages if not already done.)
o Documentation: Ensure that new functions and modules are documented using JSDoc.
o Testing: Write tests for new features and ensure that existing tests pass.
Reporting Issues
If you encounter any issues or bugs, please open an issue in the repository's Issues section with detailed information.
Style Guide
Follow the Airbnb JavaScript Style Guide for consistent code formatting.
License
By contributing, you agree that your contributions will be licensed under the project's MIT License.
Copy code
Explanation:
• Contribution Steps: Clear instructions on how to contribute via forks and pull requests.
• Coding Standards: Guidelines to maintain code quality.
• Issue Reporting: How to report bugs or request features.
• Style Guide: Reference to a standard style guide for consistency.
4.4.2. Creating LICENSE File
Choose an appropriate license for your project. The MIT License is a popular choice for open-source projects.
1. Create LICENSE:
bash
Copy code
touch LICENSE
2. Sample MIT License Content:
text
Copy code
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
________________________________________
4.5. Implementing Commit Message Guidelines
Consistent and descriptive commit messages improve project readability and traceability.
4.5.1. Adopting Conventional Commits
Conventional Commits is a specification for adding human and machine-readable meaning to commit messages.
Format:
arduino
Copy code
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types:
• feat: A new feature.
• fix: A bug fix.
• docs: Documentation only changes.
• style: Changes that do not affect the meaning of the code (white-space, formatting).
• refactor: A code change that neither fixes a bug nor adds a feature.
• test: Adding missing tests or correcting existing tests.
• chore: Changes to the build process or auxiliary tools and libraries.
Example:
bash
Copy code
git commit -m "feat(auth-service): add user registration endpoint"
4.5.2. Enforcing Commit Message Standards
While manual adherence is possible, integrating tools can automate and enforce standards.
1. Installing Commitizen (Optional):
Commitizen helps in generating standardized commit messages.
bash
Copy code
npm install --save-dev commitizen
2. Configuring Commitizen:
Add the following to your package.json:
json
Copy code
{
"scripts": {
"commit": "cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
3. Making Commits with Commitizen:
bash
Copy code
npm run commit
Follow the interactive prompts to generate a standardized commit message.
Note: While Commitizen is optional, it's beneficial for maintaining consistency, especially in larger teams.
________________________________________
4.6. Maintaining a CHANGELOG
A CHANGELOG records all notable changes made to a project, providing transparency and aiding in version tracking.
4.6.1. Creating CHANGELOG.md
1. Create the File:
bash
Copy code
touch CHANGELOG.md
2. Sample CHANGELOG.md Structure:
markdown
Copy code
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial project setup with microservices and common library.
- Authentication and User services with registration and login functionalities.
### Fixed
- None yet.
## [1.0.0] - 2024-04-27
### Added
- Complete microservices architecture for iBranDIDit.com.
- Comprehensive testing with Jest and Supertest.
- Version control and documentation setup.
3. Updating the Changelog:
o Before Each Release:
Add a new section for the upcoming release under [Unreleased].
o After Release:
Replace [Unreleased] with the new version number and date.
Update the contents accordingly.
Tip: Consider automating changelog generation using tools like standard-version in future stages.
________________________________________
4.7. Ensuring Easy Integration for Future Developers
To make the project easily accessible and understandable for new developers, ensure that all documentation is clear, up-to-date, and centralized.
4.7.1. Centralizing Documentation
1. Create a docs/ Directory:
bash
Copy code
mkdir docs
2. Link or Reference Service Documentation:
o Within the central README.md, provide links to each service's README.md and Swagger documentation.
o For more extensive documentation, consider using MkDocs or Docusaurus to create a unified documentation website.
4.7.2. Documenting API Endpoints
• Swagger Integration: Already implemented in previous stages. Ensure that Swagger documentation is comprehensive and updated with each API change.
• Sample API Documentation Access:
o Authentication Service: http://localhost:5003/api-docs
o User Service: http://localhost:5001/api-docs
o Product Service: http://localhost:5002/api-docs
o Notification Service: http://localhost:5004/api-docs
o API Gateway: Documentation may include routing configurations or general API structure.
4.7.3. Providing Setup Scripts (Optional for Future Stages)
To streamline the setup process, especially for new developers, consider creating scripts or Makefile targets that automate environment setup steps.
Example: setup.sh in Root Directory
bash
Copy code
#!/bin/bash
# Install dependencies
echo "Installing dependencies..."
npm install
# Set up environment variables (copy from example if needed)
echo "Setting up environment variables..."
cp common/.env.example common/.env
cp auth-service/.env.example auth-service/.env
cp user-service/.env.example user-service/.env
cp product-service/.env.example product-service/.env
cp notification-service/.env.example notification-service/.env
cp api-gateway/.env.example api-gateway/.env
# Start Docker services
echo "Starting Docker services..."
docker-compose up -d mongodb rabbitmq
echo "Setup complete. You can now start the services."
Usage:
1. Make the Script Executable:
bash
Copy code
chmod +x setup.sh
2. Run the Script:
bash
Copy code
./setup.sh
Note: This is optional and can be expanded based on project needs.
________________________________________
4.8. Best Practices and Tips
1. Commit Often with Meaningful Messages:
o Frequent commits help in tracking changes and isolating issues.
o Use descriptive messages to explain the purpose of each commit.
2. Review Pull Requests:
o Implement code reviews to maintain code quality.
o Use PR templates to standardize information provided during reviews.
3. Keep Documentation Updated:
o Ensure that documentation reflects the current state of the codebase.
o Update docs alongside code changes to prevent discrepancies.
4. Use Branch Protection Rules (Optional for Future Stages):
o Enforce rules like requiring PR reviews before merging.
o Protect the main branch to prevent direct pushes.
5. Tagging Releases:
o Use Git tags to mark release points.
o Follow semantic versioning for clarity.
Example: Tagging a Release
bash
Copy code
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
6. Automate Documentation Generation:
o Integrate documentation generation into your build or deploy process.
o Ensure that API docs are always up-to-date with the latest code.
________________________________________
5. Complete Code and Configuration Files
5.1. Root Directory Files
ibrandidit-microservices/.gitignore
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
docker-compose.override.yml
# OS files
.DS_Store
Thumbs.db
ibrandidit-microservices/package.json
(Assuming you have a root package.json for NPM Workspaces.)
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\""
},
"dependencies": {},
"devDependencies": {}
}
Explanation:
• private: true ensures that the root package is not accidentally published to NPM.
• workspaces: Lists all microservices and the common library, enabling centralized dependency management.
• scripts: Basic scripts for starting services and guiding testing.
ibrandidit-microservices/README.md
(Already provided in section 4.3.1. Root README.md)
ibrandidit-microservices/CONTRIBUTING.md
(Already provided in section 4.4.1)
ibrandidit-microservices/LICENSE
(Already provided in section 4.4.2)
________________________________________
5.2. Common Library (common/)
common/package.json
(Already provided in section 4.3.3)
json
Copy code
{
"name": "common",
"version": "1.0.0",
"description": "Shared models and schemas for iBranDIDit.com microservices",
"main": "index.js",
"scripts": {
"test": "jest --coverage",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"jsonschema": "^1.4.0",
"mongoose": "^7.0.3"
},
"devDependencies": {
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"supertest": "^6.3.3",
"jsdoc": "^4.0.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"models/**/*.js",
"schemas/**/*.json",
"index.js"
]
},
"config": {
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
common/.gitignore
(Already provided in section 4.1.3)
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# OS files
.DS_Store
Thumbs.db
common/index.js
(Already provided in Stage 6)
javascript
Copy code
// common/index.js
const User = require('./models/User');
const Product = require('./models/Product');
const userRegisteredSchema = require('./schemas/userRegistered.json');
const productCreatedSchema = require('./schemas/productCreated.json');
module.exports = {
User,
Product,
schemas: {
userRegistered: userRegisteredSchema,
productCreated: productCreatedSchema,
},
};
common/models/User.js
(Already provided in Stage 6)
javascript
Copy code
// common/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
});
UserSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('User', UserSchema);
common/models/Product.js
(Already provided in Stage 6)
javascript
Copy code
// common/models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
price: {
type: Number,
required: true,
min: 0
},
description: {
type: String,
trim: true
},
inventory: {
type: Number,
default: 0,
min: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
});
ProductSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Product', ProductSchema);
common/schemas/userRegistered.json
(Already provided in Stage 6)
json
Copy code
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "UserRegisteredEvent",
"type": "object",
"properties": {
"event": {
"type": "string",
"enum": ["user_registered"]
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "User ID"
},
"name": {
"type": "string",
"description": "User name"
},
"email": {
"type": "string",
"description": "User email"
}
},
"required": ["id", "name", "email"]
}
},
"required": ["event", "data"]
}
common/schemas/productCreated.json
(Already provided in Stage 6)
json
Copy code
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ProductCreatedEvent",
"type": "object",
"properties": {
"event": {
"type": "string",
"enum": ["product_created"]
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Product ID"
},
"name": {
"type": "string",
"description": "Product name"
},
"price": {
"type": "number",
"description": "Product price"
}
},
"required": ["id", "name", "price"]
}
},
"required": ["event", "data"]
}
common/__tests__/User.test.js
(Already provided in section 4.2.1.a)
javascript
Copy code
// common/__tests__/User.test.js
const mongoose = require('mongoose');
const { User } = require('../models/User');
describe('User Model Test', () => {
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/common_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
it('create & save user successfully', async () => {
const validUser = new User({
name: 'Test User',
email: 'testuser@example.com',
password: 'password123',
});
const savedUser = await validUser.save();
// Object Id should be defined when successfully saved to MongoDB.
expect(savedUser._id).toBeDefined();
expect(savedUser.name).toBe(validUser.name);
expect(savedUser.email).toBe(validUser.email);
expect(savedUser.password).toBe(validUser.password);
});
it('insert user successfully, but the field not defined in schema should be undefined', async () => {
const userWithInvalidField = new User({
name: 'Invalid User',
email: 'invaliduser@example.com',
password: 'password123',
nickname: 'Tester', // This field is not defined in the schema
});
const savedUser = await userWithInvalidField.save();
expect(savedUser._id).toBeDefined();
expect(savedUser.nickname).toBeUndefined();
});
it('create user without required field should fail', async () => {
const userWithoutRequiredField = new User({ name: 'Incomplete User' });
let err;
try {
const savedUser = await userWithoutRequiredField.save();
error = savedUser;
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
expect(err.errors.email).toBeDefined();
expect(err.errors.password).toBeDefined();
});
});
common/__tests__/Product.test.js
(Already provided in section 4.2.1.b)
javascript
Copy code
// common/__tests__/Product.test.js
const mongoose = require('mongoose');
const { Product } = require('../models/Product');
describe('Product Model Test', () => {
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/common_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
it('create & save product successfully', async () => {
const validProduct = new Product({
name: 'Test Product',
price: 99.99,
description: 'A product for testing purposes',
inventory: 50,
});
const savedProduct = await validProduct.save();
expect(savedProduct._id).toBeDefined();
expect(savedProduct.name).toBe(validProduct.name);
expect(savedProduct.price).toBe(validProduct.price);
expect(savedProduct.description).toBe(validProduct.description);
expect(savedProduct.inventory).toBe(validProduct.inventory);
});
it('create product without required fields should fail', async () => {
const productWithoutRequiredField = new Product({ description: 'No name and price' });
let err;
try {
const savedProduct = await productWithoutRequiredField.save();
error = savedProduct;
} catch (error) {
err = error;
}
expect(err).toBeInstanceOf(mongoose.Error.ValidationError);
expect(err.errors.name).toBeDefined();
expect(err.errors.price).toBeDefined();
});
});
common/__tests__/EventSchemaValidation.test.js
(Already provided in section 4.2.4)
javascript
Copy code
// common/__tests__/EventSchemaValidation.test.js
const { Validator } = require('jsonschema');
const { schemas } = require('../index');
const validator = new Validator();
describe('Event Schema Validation', () => {
it('should validate user_registered event successfully', () => {
const validEvent = {
event: 'user_registered',
data: {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Eve',
email: 'eve@example.com',
},
};
const validation = validator.validate(validEvent, schemas.userRegistered);
expect(validation.valid).toBe(true);
});
it('should fail validation for user_registered event with missing fields', () => {
const invalidEvent = {
event: 'user_registered',
data: {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
// Missing name and email
},
};
const validation = validator.validate(invalidEvent, schemas.userRegistered);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
it('should validate product_created event successfully', () => {
const validEvent = {
event: 'product_created',
data: {
id: '60d5fb0ab1e8e1d4c8e4f8c7',
name: 'Hat',
price: 19.99,
},
};
const validation = validator.validate(validEvent, schemas.productCreated);
expect(validation.valid).toBe(true);
});
it('should fail validation for product_created event with incorrect data types', () => {
const invalidEvent = {
event: 'product_created',
data: {
id: '60d5fb0ab1e8e1d4c8e4f8c7',
name: 'Hat',
price: '19.99', // Should be a number
},
};
const validation = validator.validate(invalidEvent, schemas.productCreated);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
});
________________________________________
5.3. Authentication Service (auth-service/)
(Already provided in previous sections. Below is a summary of key files.)
auth-service/package.json
(Already provided in section 4.3.3)
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"docs": "jsdoc -c jsdoc.json",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"nodemon": "^3.0.1",
"supertest": "^6.3.3",
"jsdoc": "^4.0.0",
"cz-conventional-changelog": "^3.3.0",
"commitizen": "^4.3.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
auth-service/README.md
(Already provided in section 4.3.2.a)
auth-service/.gitignore
(Already provided in section 4.1.3)
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
Dockerfile
# OS files
.DS_Store
Thumbs.db
auth-service/index.js
(Already provided in Stage 4.3.2.b)
javascript
Copy code
// auth-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Auth Service connected to MongoDB'))
.catch((err) => console.error('Auth Service failed to connect to MongoDB:', err));
// Routes
app.use('/auth', authRoutes);
app.use('/users', userRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Authentication Service API');
});
// Start Server
const PORT = process.env.PORT || 5003;
app.listen(PORT, () => {
console.log(`Auth Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
auth-service/jest.config.js
(Already provided in section 4.3.3)
javascript
Copy code
// auth-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
auth-service/jest.setup.js
(Already provided in section 4.5.1)
javascript
Copy code
// auth-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
auth-service/swagger.js
(Already provided in Stage 6)
javascript
Copy code
// auth-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Authentication Service API',
version: '1.0.0',
description: 'API documentation for the Authentication Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5003',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
RegisterInput: {
type: 'object',
required: ['name', 'email', 'password'],
properties: {
name: {
type: 'string',
description: 'The name of the user',
},
email: {
type: 'string',
description: 'The email of the user',
},
password: {
type: 'string',
description: 'The password of the user',
},
},
example: {
name: 'Alice',
email: 'alice@example.com',
password: 'securepassword',
},
},
LoginInput: {
type: 'object',
required: ['email', 'password'],
properties: {
email: {
type: 'string',
description: 'The email of the user',
},
password: {
type: 'string',
description: 'The password of the user',
},
},
example: {
email: 'alice@example.com',
password: 'securepassword',
},
},
UserOutput: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'User ID',
},
name: {
type: 'string',
description: 'User name',
},
email: {
type: 'string',
description: 'User email',
},
},
},
LoginOutput: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Login message',
},
token: {
type: 'string',
description: 'JWT token',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
auth-service/tests/auth.test.js
(Already provided in section 4.2.1.a and modified in 4.3.3.a)
________________________________________
5.4. User Service (user-service/)
(Similarly, follow the structure provided for auth-service.)
user-service/package.json
(Already provided in section 4.3.3)
json
Copy code
{
"name": "user-service",
"version": "1.0.0",
"description": "User Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"docs": "jsdoc -c jsdoc.json",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"nodemon": "^3.0.1",
"supertest": "^6.3.3",
"jsdoc": "^4.0.0",
"cz-conventional-changelog": "^3.3.0",
"commitizen": "^4.3.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
user-service/README.md
markdown
Copy code
# User Service
## Overview
The User Service manages user profiles, including creation, retrieval, and updates. It interacts with the **Common** library for shared models and schemas and communicates with other services via RabbitMQ.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Contributing](#contributing)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [Docker](https://www.docker.com/get-started) (optional, if running via Docker)
- [Docker Compose](https://docs.docker.com/compose/install/) (optional)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd user-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-service/ directory based on the provided .env.example.
env
Copy code
PORT=5001
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/userdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/user_profile_test
Descriptions:
• PORT: Port on which the service runs.
• RABBITMQ_URL: Connection string for RabbitMQ.
• MONGODB_URI: Connection string for the production MongoDB database.
• MONGODB_URI_TEST: Connection string for the test MongoDB database.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
1. Start the Service:
bash
Copy code
docker-compose up --build user-service
Option 2: Running Locally
1. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
2. Start the Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
• URL: http://localhost:5001/api-docs
Testing
Run tests using Jest:
bash
Copy code
npm test
Coverage Reports:
After running tests, view coverage reports in the coverage/ directory.
Contributing
Contributions are welcome! Please refer to the root README for guidelines.
________________________________________
bash
Copy code
#### **`user-service/.gitignore`**
*(Already provided in section 4.1.3)*
```gitignore
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
Dockerfile
# OS files
.DS_Store
Thumbs.db
user-service/index.js
(Already provided in section 4.3.3.b)
javascript
Copy code
// user-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const userRoutes = require('./routes/userRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('User Service connected to MongoDB'))
.catch((err) => console.error('User Service failed to connect to MongoDB:', err));
// Routes
app.use('/users', userRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to User Service API');
});
// Start Server
const PORT = process.env.PORT || 5001;
app.listen(PORT, () => {
console.log(`User Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
user-service/jest.config.js
(Already provided in section 4.3.3)
javascript
Copy code
// user-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
user-service/jest.setup.js
(Already provided in section 4.5.1)
javascript
Copy code
// user-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
user-service/swagger.js
(Already provided in Stage 6)
javascript
Copy code
// user-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com User Service API',
version: '1.0.0',
description: 'API documentation for the User Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5001',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
UserProfileInput: {
type: 'object',
required: ['userId'],
properties: {
userId: {
type: 'string',
description: 'User ID referencing the User model',
},
address: {
type: 'string',
description: 'User address',
},
phone: {
type: 'string',
description: 'User phone number',
},
},
example: {
userId: '60d5f9f9b1e8e1d4c8e4f8c5',
address: '123 Test Street',
phone: '555-1234',
},
},
UserProfileOutput: {
type: 'object',
properties: {
_id: {
type: 'string',
description: 'UserProfile ID',
},
userId: {
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
address: {
type: 'string',
description: 'User address',
},
phone: {
type: 'string',
description: 'User phone number',
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation timestamp',
},
updatedAt: {
type: 'string',
format: 'date-time',
description: 'Update timestamp',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
user-service/tests/user.test.js
(Already provided in section 4.3.3.d)
javascript
Copy code
// user-service/tests/user.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { User } = require('common');
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/user_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Test User',
email: 'testuser@example.com',
password: 'testpassword',
});
// Extract user ID from register response
testUserId = registerRes.body.user.id;
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'testuser@example.com',
password: 'testpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await mongoose.connection.db.dropDatabase();
await mongoose.connection.close();
});
describe('User Service API Endpoints', () => {
it('should create a new user profile', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId,
address: '123 Test Street',
phone: '555-1234',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUserId);
expect(res.body).toHaveProperty('address', '123 Test Street');
expect(res.body).toHaveProperty('phone', '555-1234');
});
it('should not create a user profile with existing userId', async () => {
// Attempt to create the same profile again
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId, // Same userId
address: '123 Test Street',
phone: '555-1234',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User profile already exists');
});
it('should retrieve a list of user profiles', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('userId');
expect(res.body[0].userId).toHaveProperty('name', 'Test User');
expect(res.body[0].userId).toHaveProperty('email', 'testuser@example.com');
});
it('should retrieve a user profile by user ID', async () => {
const res = await request(app)
.get(`/users/${testUserId}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('userId');
expect(res.body.userId).toHaveProperty('name', 'Test User');
expect(res.body.userId).toHaveProperty('email', 'testuser@example.com');
});
it('should not retrieve a non-existent user profile', async () => {
const res = await request(app)
.get('/users/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'User profile not found');
});
});
user-service/tests/userProfile.test.js
(Already provided in section 4.3.3.d)
javascript
Copy code
// user-service/tests/userProfile.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { User } = require('common');
const UserProfile = require('../models/UserProfile');
const bcrypt = require('bcrypt');
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/user_profile_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Test User',
email: 'testuser@example.com',
password: 'testpassword',
});
// Extract user ID from register response
testUserId = registerRes.body.user.id;
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'testuser@example.com',
password: 'testpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await UserProfile.deleteMany({});
await User.deleteMany({});
await mongoose.connection.close();
});
describe('User Profile Service API Endpoints', () => {
it('should create a new user profile', async () => {
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId,
address: '123 Test Street',
phone: '555-1234',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUserId);
expect(res.body).toHaveProperty('address', '123 Test Street');
expect(res.body).toHaveProperty('phone', '555-1234');
});
it('should not create a user profile with existing userId', async () => {
// Attempt to create the same profile again
const res = await request(app)
.post('/users')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUserId, // Same userId
address: '123 Test Street',
phone: '555-1234',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User profile already exists');
});
it('should retrieve a list of user profiles', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('userId');
expect(res.body[0].userId).toHaveProperty('name', 'Test User');
expect(res.body[0].userId).toHaveProperty('email', 'testuser@example.com');
});
it('should retrieve a user profile by user ID', async () => {
const res = await request(app)
.get(`/users/${testUserId}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('userId');
expect(res.body.userId).toHaveProperty('name', 'Test User');
expect(res.body.userId).toHaveProperty('email', 'testuser@example.com');
});
it('should not retrieve a non-existent user profile', async () => {
const res = await request(app)
.get('/users/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'User profile not found');
});
});
________________________________________
5.5. Product Service (product-service/)
(Follow similar patterns as auth-service and user-service.)
product-service/package.json
(Already provided in section 4.3.3)
json
Copy code
{
"name": "product-service",
"version": "1.0.0",
"description": "Product Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"docs": "jsdoc -c jsdoc.json",
"swagger-autogen": "node swagger.js"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3",
"common": "1.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"nodemon": "^3.0.1",
"supertest": "^6.3.3",
"jsdoc": "^4.0.0",
"cz-conventional-changelog": "^3.3.0",
"commitizen": "^4.3.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
product-service/README.md
markdown
Copy code
# Product Service
## Overview
The Product Service manages the product catalog, including creation, retrieval, and inventory management. It interacts with the **Common** library for shared models and schemas and communicates with other services via RabbitMQ.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Contributing](#contributing)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [Docker](https://www.docker.com/get-started) (optional, if running via Docker)
- [Docker Compose](https://docs.docker.com/compose/install/) (optional)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd product-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the product-service/ directory based on the provided .env.example.
env
Copy code
PORT=5002
NODE_ENV=development
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
MONGODB_URI_TEST=mongodb://localhost:27017/product_test
Descriptions:
• PORT: Port on which the service runs.
• RABBITMQ_URL: Connection string for RabbitMQ.
• MONGODB_URI: Connection string for the production MongoDB database.
• MONGODB_URI_TEST: Connection string for the test MongoDB database.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
1. Start the Service:
bash
Copy code
docker-compose up --build product-service
Option 2: Running Locally
1. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
2. Start the Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
• URL: http://localhost:5002/api-docs
Testing
Run tests using Jest:
bash
Copy code
npm test
Coverage Reports:
After running tests, view coverage reports in the coverage/ directory.
Contributing
Contributions are welcome! Please refer to the root README for guidelines.
________________________________________
bash
Copy code
#### **`product-service/.gitignore`**
*(Already provided in section 4.1.3)*
```gitignore
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
Dockerfile
# OS files
.DS_Store
Thumbs.db
product-service/index.js
(Already provided in section 4.3.3.b)
javascript
Copy code
// product-service/index.js
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const productRoutes = require('./routes/productRoutes');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Product Service connected to MongoDB'))
.catch((err) => console.error('Product Service failed to connect to MongoDB:', err));
// Routes
app.use('/products', productRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Product Service API');
});
// Start Server
const PORT = process.env.PORT || 5002;
app.listen(PORT, () => {
console.log(`Product Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
product-service/jest.config.js
(Already provided in section 4.3.3)
javascript
Copy code
// product-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
product-service/jest.setup.js
(Already provided in section 4.5.1)
javascript
Copy code
// product-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
product-service/swagger.js
(Already provided in Stage 6)
javascript
Copy code
// product-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Product Service API',
version: '1.0.0',
description: 'API documentation for the Product Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5002',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
ProductInput: {
type: 'object',
required: ['name', 'price'],
properties: {
name: {
type: 'string',
description: 'Product name',
},
price: {
type: 'number',
description: 'Product price',
},
description: {
type: 'string',
description: 'Product description',
},
inventory: {
type: 'number',
description: 'Number of items in stock',
},
},
example: {
name: 'Sneakers',
price: 49.99,
description: 'Comfortable running sneakers',
inventory: 150,
},
},
ProductOutput: {
type: 'object',
properties: {
_id: {
type: 'string',
description: 'Product ID',
},
name: {
type: 'string',
description: 'Product name',
},
price: {
type: 'number',
description: 'Product price',
},
description: {
type: 'string',
description: 'Product description',
},
inventory: {
type: 'number',
description: 'Number of items in stock',
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Creation timestamp',
},
updatedAt: {
type: 'string',
format: 'date-time',
description: 'Update timestamp',
},
},
},
},
},
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
product-service/tests/product.test.js
(Already provided in section 4.3.3.d)
javascript
Copy code
// product-service/tests/product.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { Product } = require('common');
const bcrypt = require('bcrypt');
let token;
let testUserId;
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/product_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Create a test user in auth-service and get token
const registerRes = await request('http://localhost:5003')
.post('/auth/register')
.send({
name: 'Product Tester',
email: 'producttester@example.com',
password: 'productpassword',
});
// Extract user ID from register response
testUserId = registerRes.body.user.id;
// Login to get token
const loginRes = await request('http://localhost:5003')
.post('/auth/login')
.send({
email: 'producttester@example.com',
password: 'productpassword',
});
token = loginRes.body.token;
});
afterAll(async () => {
// Clean up database after tests
await Product.deleteMany({});
await mongoose.connection.close();
});
describe('Product Service API Endpoints', () => {
it('should create a new product', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Test Product',
price: 99.99,
description: 'A product for testing purposes',
inventory: 100,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
expect(res.body).toHaveProperty('description', 'A product for testing purposes');
expect(res.body).toHaveProperty('inventory', 100);
});
it('should not create a product without name and price', async () => {
const res = await request(app)
.post('/products')
.send({
description: 'Missing name and price',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Name and price are required');
});
it('should retrieve a list of products', async () => {
const res = await request(app)
.get('/products');
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body[0]).toHaveProperty('name', 'Test Product');
});
it('should retrieve a product by ID', async () => {
// Fetch the product created earlier
const products = await Product.find().limit(1);
const productId = products[0]._id.toString();
const res = await request(app)
.get(`/products/${productId}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('name', 'Test Product');
expect(res.body).toHaveProperty('price', 99.99);
});
it('should not retrieve a non-existent product', async () => {
const res = await request(app)
.get('/products/60c72b2f9b1e8e1d4c8e4f8d'); // Assuming this ID doesn't exist
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Product not found');
});
});
product-service/tests/productIntegration.test.js
(Already provided in section 4.3.3.d)
javascript
Copy code
// product-service/tests/productIntegration.test.js
const request = require('supertest');
const app = require('../index');
const mongoose = require('mongoose');
const { Product } = require('common');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
describe('Product Service Integration Tests', () => {
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
it('should create a new product and publish event', async () => {
const res = await request(app)
.post('/products')
.send({
name: 'Test Product Integration',
price: 59.99,
description: 'Integration test product',
inventory: 30,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('name', 'Test Product Integration');
expect(res.body).toHaveProperty('price', 59.99);
expect(res.body).toHaveProperty('description', 'Integration test product');
expect(res.body).toHaveProperty('inventory', 30);
// Optionally, verify that the event was published to RabbitMQ
// This requires mocking or spying on the rabbitmqService.publishEvent method
// For simplicity, this step can be skipped or handled with more advanced setups
});
it('should retrieve all products', async () => {
const res = await request(app).get('/products');
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBe(1);
expect(res.body[0]).toHaveProperty('name', 'Test Product Integration');
});
});
________________________________________
5.6. API Gateway (api-gateway/)
api-gateway/package.json
(Already provided in section 4.3.3)
json
Copy code
{
"name": "api-gateway",
"version": "1.0.0",
"description": "API Gateway for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "express-gateway start",
"test": "jest --coverage",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"express-gateway": "^2.30.0",
"jsonwebtoken": "^9.0.0",
"amqplib": "^0.10.3",
"common": "1.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.0",
"jest": "^29.6.1",
"mongodb-memory-server": "^8.12.2",
"supertest": "^6.3.3",
"jsdoc": "^4.0.0",
"cz-conventional-changelog": "^3.3.0",
"commitizen": "^4.3.0"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
api-gateway/README.md
markdown
Copy code
# API Gateway
## Overview
The API Gateway serves as the entry point for all client requests, handling routing, authentication, and aggregating responses from various microservices. It utilizes **Express Gateway** for managing API endpoints and policies.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [Configuration](#configuration)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Contributing](#contributing)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [Docker](https://www.docker.com/get-started) (optional, if running via Docker)
- [Docker Compose](https://docs.docker.com/compose/install/) (optional)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd api-gateway
2. Install Dependencies:
bash
Copy code
npm install
3. Install Express Gateway Globally (if not already installed):
bash
Copy code
npm install -g express-gateway
Environment Variables
Create a .env file in the api-gateway/ directory based on the provided .env.example.
env
Copy code
PORT=8080
NODE_ENV=development
JWT_SECRET=your_jwt_secret_key
Descriptions:
• PORT: Port on which the gateway runs.
• JWT_SECRET: Secret key used to verify JWT tokens (should match the one in auth-service).
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
1. Start the Service:
bash
Copy code
docker-compose up --build api-gateway
Option 2: Running Locally
1. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
2. Start the Service:
bash
Copy code
npm run start
Note: The start script uses Express Gateway to run the API Gateway.
Configuration
Express Gateway uses a configuration file (gateway.config.yml) to define API endpoints, service routing, and policies.
gateway.config.yml Example:
yaml
Copy code
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- jwt-auth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- jwt-auth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
Explanation:
• http.port: Port on which the gateway listens.
• apiEndpoints: Defines API endpoints and routes them to respective services.
• serviceEndpoints: Specifies the URLs of the microservices.
• policies: Middleware policies applied to requests (e.g., jwt-auth for authentication).
• pipelines: Chains policies for specific API endpoints.
Customization:
• Replace 'your_jwt_secret_key' with the actual JWT_SECRET from auth-service/.env.
• Adjust service URLs if running microservices on different hosts or ports.
API Documentation
While the API Gateway primarily manages routing and authentication, its configuration can be documented similarly using JSDoc or integrated into the central documentation.
Testing
Run tests using Jest:
bash
Copy code
npm test
Note: Testing the API Gateway's routing and policies may require mocking or spinning up dependent services. For comprehensive integration testing, consider using tools like Supertest in combination with Jest.
Contributing
Contributions are welcome! Please refer to the root README for guidelines.
________________________________________
bash
Copy code
#### **`api-gateway/.gitignore`**
*(Already provided in section 4.1.3)*
```gitignore
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# Docker files
Dockerfile
# OS files
.DS_Store
Thumbs.db
api-gateway/gateway.config.yml
(Already provided in section 4.3.3.b)
yaml
Copy code
# api-gateway/config/gateway.config.yml
http:
port: 8080
apiEndpoints:
users:
path: /users
target: http://user-service:5001
methods: GET, POST
authentication:
- jwt-auth
userById:
path: /users/:id
target: http://user-service:5001
methods: GET
authentication:
- jwt-auth
products:
path: /products
target: http://product-service:5002
methods: GET, POST
productById:
path: /products/:id
target: http://product-service:5002
methods: GET
serviceEndpoints:
user-service:
url: 'http://user-service:5001'
product-service:
url: 'http://product-service:5002'
auth-service:
url: 'http://auth-service:5003'
policies:
- proxy
- jwt-auth
pipelines:
usersPipeline:
apiEndpoints:
- users
- userById
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: user-service
productsPipeline:
apiEndpoints:
- products
- productById
policies:
- proxy:
- action:
serviceEndpoint: product-service
api-gateway/index.js
Not typically needed for Express Gateway, but if you have custom middleware or routes, include them here.
api-gateway/jest.config.js
(Already provided in section 4.3.3)
javascript
Copy code
// api-gateway/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js'
]
};
api-gateway/jest.setup.js
(Already provided in section 4.5.1)
javascript
Copy code
// api-gateway/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
api-gateway/swagger.js
Express Gateway does not inherently use Swagger for API documentation, but you can document custom routes or configurations as needed. Alternatively, rely on individual microservices' Swagger docs.
________________________________________
5.7. Additional Tools and Best Practices
1. Linting and Formatting:
Implement linting tools like ESLint and formatting tools like Prettier to maintain consistent code styles.
o Installation:
bash
Copy code
npm install --save-dev eslint prettier
o Configuration:
Create .eslintrc.json and .prettierrc files with your preferred settings.
o Adding Scripts:
json
Copy code
{
"scripts": {
"lint": "eslint . --ext .js",
"format": "prettier --write ."
}
}
2. Automating Documentation Generation:
Integrate documentation generation into your CI/CD pipeline (to be addressed in future stages).
3. Utilizing GitHub Features:
o Issues: Track bugs and feature requests.
o Projects: Manage project tasks and workflows.
o Wiki: Host detailed documentation (optional).
4. Security Best Practices:
o Never Commit Sensitive Information: Ensure .env files are excluded via .gitignore.
o Use Environment Variables: Manage secrets securely, especially in production environments.
5. Maintaining a Clean Commit History:
o Rebase Feature Branches: Keep a linear commit history by rebasing before merging PRs.
o Squash Commits: Combine multiple commits into one before merging for clarity.
________________________________________
6. Running the Complete Project with Version Control & Documentation
1. Ensure All Services Are Set Up:
o Each microservice has its own README.md, package.json, .gitignore, and necessary configuration files.
o The root README.md provides an overview and links to individual services.
2. Start All Services Using Docker Compose:
bash
Copy code
docker-compose up --build
3. Access API Documentation:
o Authentication Service: http://localhost:5003/api-docs
o User Service: http://localhost:5001/api-docs
o Product Service: http://localhost:5002/api-docs
o Notification Service: http://localhost:5004/api-docs
o API Gateway: No inherent Swagger docs, refer to individual services.
4. Contribute to the Project:
o Clone the Repository:
bash
Copy code
git clone https://github.com/yourusername/ibrandidit-microservices.git
cd ibrandidit-microservices/
o Create a Feature Branch:
bash
Copy code
git checkout -b feature/add-new-feature
o Make Changes, Commit, and Push:
bash
Copy code
git add .
git commit -m "feat(auth-service): add new authentication feature"
git push origin feature/add-new-feature
o Create a Pull Request: On GitHub, navigate to the repository and create a PR from your feature branch to main.
________________________________________
7. Troubleshooting Tips
1. Git Issues:
o Authentication Errors: Ensure you have the correct permissions and are using the right SSH keys or HTTPS credentials.
o Merge Conflicts: Regularly pull updates from main and rebase your feature branches to minimize conflicts.
2. Documentation Not Updating:
o Run Documentation Scripts: Ensure you run npm run docs after making changes.
o Check Configuration Files: Verify that jsdoc.json paths and settings are correct.
3. CI/CD Pipeline Failures (To Be Implemented in Future Stages):
o Check Logs: Review CI/CD logs to identify errors.
o Environment Variables: Ensure all necessary variables are set in the CI/CD environment.
4. Service Communication Issues:
o Check Docker Networks: Ensure all services are on the same network and can communicate.
o Verify Service Endpoints: Confirm that service URLs in gateway.config.yml are correct.
5. Swagger UI Not Loading:
o Service Running: Ensure the respective service is up and accessible.
o Correct Port: Verify that you're accessing the correct port for Swagger UI.
________________________________________
8. Next Steps
With Stage 8: Version Control & Documentation completed, your project now boasts a robust version control system and comprehensive documentation, laying a solid foundation for collaboration and future development. This setup ensures that your codebase remains organized, changes are tracked meticulously, and new developers can onboard with ease.
Proceed to Stage 9: Continuous Integration and Deployment (CI/CD)
In the next stage, we'll implement Continuous Integration (CI) and Continuous Deployment (CD) pipelines to automate testing, building, and deployment processes, enhancing the efficiency and reliability of your development workflow.
________________________________________
9. Additional Resources
• Git Documentation: https://git-scm.com/doc
• GitHub Guides: https://guides.github.com/
• JSDoc Documentation: https://jsdoc.app/
• Express Gateway Documentation: https://www.express-gateway.io/docs/
• Conventional Commits: https://www.conventionalcommits.org/en/v1.0.0/
• Semantic Versioning: https://semver.org/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for version control and documentation within a microservices architecture. Depending on project complexity and team size, further optimizations and configurations may be necessary, such as implementing more advanced documentation systems, integrating with project management tools, or establishing stricter contribution guidelines.
________________________________________
By following this comprehensive guide for Stage 8: Version Control & Documentation, you've ensured that your microservices architecture is well-managed, changes are tracked efficiently, and all team members have access to the necessary information to contribute effectively. This advancement sets the stage for further enhancements, ensuring your eCommerce platform remains scalable, maintainable, and robust.
Stage 9: Modular Frontend Development is an exciting step towards creating a comprehensive, user-friendly interface for your eCommerce platform, iBranDIDit.com. This stage focuses on building a modular, scalable, and maintainable frontend application that interacts seamlessly with the backend microservices you've established in previous stages.
________________________________________
Stage 9: Modular Frontend Development
________________________________________
1. Overview
Objective:
• Build a Modular Frontend: Develop a frontend application using a component-based framework that promotes reusability and scalability.
• Integrate with Backend Microservices: Connect the frontend to existing backend services via the API Gateway.
• Implement Authentication: Handle user authentication and authorization using JWT.
• Ensure Easy Integration: Set up the project structure and configurations to allow easy deployment and content updates.
Benefits:
• User-Friendly Interface: Provides an intuitive and responsive user experience for customers.
• Maintainability: Modular components make the codebase easier to manage and extend.
• Scalability: Facilitates the addition of new features and pages without significant refactoring.
• Seamless Integration: Ensures smooth communication with backend services, enabling full functionality of the eCommerce platform.
________________________________________
2. Technology Stack
• Framework: React – A popular JavaScript library for building user interfaces.
• Package Manager: NPM – Manages frontend dependencies.
• Module Bundler: Webpack – Bundles JavaScript files for usage in a browser (handled by Create React App).
• Styling:
o CSS Modules: For scoped and modular CSS.
o Bootstrap: For responsive design and pre-built UI components.
• Routing: React Router – Manages client-side routing.
• State Management: React Context API – For managing global state.
• HTTP Client: Axios – For making API requests.
• Authentication: JWT – For handling user authentication tokens.
• Build Tool: Create React App – Bootstraps the React application with sensible defaults.
• Version Control: Git – Continuation from Stage 8.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Documentation: Markdown – For writing comprehensive README and component documentation.
________________________________________
3. Implementation Steps
3.1. Setting Up the Frontend Project
3.1.1. Creating the Frontend Directory
1. Navigate to the Root Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Create the Frontend Workspace:
We'll use NPM Workspaces to manage the frontend alongside existing microservices.
bash
Copy code
mkdir frontend
3. Initialize a New React Application:
Use Create React App for a quick and standardized setup.
bash
Copy code
npx create-react-app frontend --template cra-template
Explanation:
o npx create-react-app frontend initializes a new React project in the frontend/ directory.
o --template cra-template uses the default Create React App template.
3.1.2. Configuring NPM Workspaces
1. Update Root package.json:
Add the frontend to the workspaces array.
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service",
"frontend"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\""
},
"dependencies": {},
"devDependencies": {}
}
2. Install Dependencies:
Navigate back to the root directory and install all dependencies across workspaces.
bash
Copy code
cd frontend
npm install
cd ..
npm install
Note: This ensures that all dependencies are correctly linked and managed within the monorepo.
3.1.3. Setting Up .gitignore for Frontend
1. Create Frontend .gitignore:
Navigate to the frontend/ directory and ensure the .gitignore is set to exclude unnecessary files.
bash
Copy code
cd frontend
2. Add .gitignore Content:
gitignore
Copy code
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
3. Return to Root Directory:
bash
Copy code
cd ..
3.2. Project Structure Overview
The frontend/ directory will have the following structure:
java
Copy code
frontend/
├── public/
│ ├── index.html
│ └── ...
├── src/
│ ├── components/
│ │ ├── Navbar/
│ │ │ ├── Navbar.js
│ │ │ └── Navbar.module.css
│ │ ├── Footer/
│ │ │ ├── Footer.js
│ │ │ └── Footer.module.css
│ │ └── ... (other reusable components)
│ ├── pages/
│ │ ├── Home/
│ │ │ ├── Home.js
│ │ │ └── Home.module.css
│ │ ├── Login/
│ │ │ ├── Login.js
│ │ │ └── Login.module.css
│ │ ├── Register/
│ │ │ ├── Register.js
│ │ │ └── Register.module.css
│ │ ├── Products/
│ │ │ ├── Products.js
│ │ │ └── Products.module.css
│ │ ├── ProductDetail/
│ │ │ ├── ProductDetail.js
│ │ │ └── ProductDetail.module.css
│ │ ├── Profile/
│ │ │ ├── Profile.js
│ │ │ └── Profile.module.css
│ │ └── ... (other pages)
│ ├── context/
│ │ ├── AuthContext.js
│ │ └── ... (other contexts)
│ ├── services/
│ │ ├── api.js
│ │ └── auth.js
│ ├── App.js
│ ├── index.js
│ └── ... (other configurations)
├── .gitignore
├── package.json
├── README.md
└── ... (other configuration files)
Explanation:
• components/: Contains reusable UI components like Navbar, Footer, Buttons, etc.
• pages/: Contains page-level components corresponding to different routes.
• context/: Manages global state using React Context API (e.g., AuthContext).
• services/: Handles API interactions and authentication logic.
• App.js: Sets up routing and global providers.
• index.js: Entry point of the React application.
3.3. Developing the Frontend Components
3.3.1. Setting Up Routing with React Router
1. Install React Router:
bash
Copy code
cd frontend
npm install react-router-dom
2. Configure App.js:
javascript
Copy code
// frontend/src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar/Navbar';
import Footer from './components/Footer/Footer';
import Home from './pages/Home/Home';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Products from './pages/Products/Products';
import ProductDetail from './pages/ProductDetail/ProductDetail';
import Profile from './pages/Profile/Profile';
import ProtectedRoute from './components/ProtectedRoute/ProtectedRoute';
import './App.css';
function App() {
return (
<Router>
<Navbar />
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
{/* Add more routes as needed */}
</Routes>
</div>
<Footer />
</Router>
);
}
export default App;
3. Creating ProtectedRoute Component:
This component ensures that only authenticated users can access certain routes.
javascript
Copy code
// frontend/src/components/ProtectedRoute/ProtectedRoute.js
import React, { useContext } from 'react';
import { Navigate } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';
const ProtectedRoute = ({ children }) => {
const { isAuthenticated } = useContext(AuthContext);
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return children;
};
export default ProtectedRoute;
3.3.2. Implementing Authentication Context
1. Create AuthContext.js:
javascript
Copy code
// frontend/src/context/AuthContext.js
import React, { createContext, useState, useEffect } from 'react';
import axios from 'axios';
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
// Optionally verify token with backend
axios
.get('/auth/verify', {
headers: { Authorization: `Bearer ${token}` },
})
.then((response) => {
setIsAuthenticated(true);
setUser(response.data.user);
})
.catch((error) => {
console.error('Token verification failed:', error);
setIsAuthenticated(false);
setUser(null);
localStorage.removeItem('token');
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, []);
const login = (token, userData) => {
localStorage.setItem('token', token);
setIsAuthenticated(true);
setUser(userData);
};
const logout = () => {
localStorage.removeItem('token');
setIsAuthenticated(false);
setUser(null);
};
return (
<AuthContext.Provider value={{ isAuthenticated, user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
2. Wrap App.js with AuthProvider:
javascript
Copy code
// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import AuthProvider from './context/AuthContext';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
3.3.3. Creating Reusable Components
1. Navbar Component:
javascript
Copy code
// frontend/src/components/Navbar/Navbar.js
import React, { useContext } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { AuthContext } from '../../context/AuthContext';
import styles from './Navbar.module.css';
const Navbar = () => {
const { isAuthenticated, user, logout } = useContext(AuthContext);
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<nav className={styles.navbar}>
<div className={styles.logo}>
<Link to="/">iBranDIDit.com</Link>
</div>
<ul className={styles.navLinks}>
<li>
<Link to="/products">Products</Link>
</li>
{isAuthenticated ? (
<>
<li>
<Link to="/profile">Profile</Link>
</li>
<li>
<button onClick={handleLogout} className={styles.logoutButton}>
Logout
</button>
</li>
</>
) : (
<>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/register">Register</Link>
</li>
</>
)}
</ul>
</nav>
);
};
export default Navbar;
2. Navbar Styling:
css
Copy code
/* frontend/src/components/Navbar/Navbar.module.css */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #343a40;
color: #fff;
}
.logo a {
color: #fff;
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.navLinks {
list-style: none;
display: flex;
gap: 1rem;
}
.navLinks li a {
color: #fff;
text-decoration: none;
font-size: 1rem;
}
.logoutButton {
background: none;
border: none;
color: #fff;
cursor: pointer;
font-size: 1rem;
}
.logoutButton:hover {
text-decoration: underline;
}
3. Footer Component:
javascript
Copy code
// frontend/src/components/Footer/Footer.js
import React from 'react';
import styles from './Footer.module.css';
const Footer = () => {
return (
<footer className={styles.footer}>
<p>© {new Date().getFullYear()} iBranDIDit.com. All rights reserved.</p>
</footer>
);
};
export default Footer;
4. Footer Styling:
css
Copy code
/* frontend/src/components/Footer/Footer.module.css */
.footer {
text-align: center;
padding: 1rem 0;
background-color: #f8f9fa;
position: fixed;
bottom: 0;
width: 100%;
}
3.3.4. Building Page Components
1. Home Page:
javascript
Copy code
// frontend/src/pages/Home/Home.js
import React from 'react';
import styles from './Home.module.css';
const Home = () => {
return (
<div className={styles.home}>
<h1>Welcome to iBranDIDit.com</h1>
<p>Your one-stop shop for all your needs.</p>
</div>
);
};
export default Home;
2. Home Page Styling:
css
Copy code
/* frontend/src/pages/Home/Home.module.css */
.home {
padding: 2rem;
text-align: center;
}
.home h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.home p {
font-size: 1.2rem;
color: #6c757d;
}
3. Login Page:
javascript
Copy code
// frontend/src/pages/Login/Login.js
import React, { useState, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { AuthContext } from '../../context/AuthContext';
import styles from './Login.module.css';
const Login = () => {
const navigate = useNavigate();
const { login } = useContext(AuthContext);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('/auth/login', { email, password });
login(response.data.token, response.data.user);
navigate('/');
} catch (err) {
setError(err.response?.data?.message || 'Login failed');
}
};
return (
<div className={styles.loginContainer}>
<h2>Login</h2>
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className={styles.submitButton}>
Login
</button>
</form>
</div>
);
};
export default Login;
4. Login Page Styling:
css
Copy code
/* frontend/src/pages/Login/Login.module.css */
.loginContainer {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.loginContainer h2 {
text-align: center;
margin-bottom: 1.5rem;
}
.error {
color: red;
text-align: center;
margin-bottom: 1rem;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.formGroup {
display: flex;
flex-direction: column;
}
.formGroup label {
margin-bottom: 0.5rem;
font-weight: bold;
}
.formGroup input {
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.submitButton {
padding: 0.75rem;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 1rem;
}
.submitButton:hover {
background-color: #0056b3;
}
5. Register Page:
javascript
Copy code
// frontend/src/pages/Register/Register.js
import React, { useState, useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { AuthContext } from '../../context/AuthContext';
import styles from './Register.module.css';
const Register = () => {
const navigate = useNavigate();
const { login } = useContext(AuthContext);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('/auth/register', { name, email, password });
login(response.data.token, response.data.user);
navigate('/');
} catch (err) {
setError(err.response?.data?.message || 'Registration failed');
}
};
return (
<div className={styles.registerContainer}>
<h2>Register</h2>
{error && <p className={styles.error}>{error}</p>}
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className={styles.submitButton}>
Register
</button>
</form>
</div>
);
};
export default Register;
6. Register Page Styling:
css
Copy code
/* frontend/src/pages/Register/Register.module.css */
.registerContainer {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.registerContainer h2 {
text-align: center;
margin-bottom: 1.5rem;
}
.error {
color: red;
text-align: center;
margin-bottom: 1rem;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.formGroup {
display: flex;
flex-direction: column;
}
.formGroup label {
margin-bottom: 0.5rem;
font-weight: bold;
}
.formGroup input {
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 0.25rem;
}
.submitButton {
padding: 0.75rem;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 1rem;
}
.submitButton:hover {
background-color: #218838;
}
7. Products Page:
javascript
Copy code
// frontend/src/pages/Products/Products.js
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import styles from './Products.module.css';
const Products = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await axios.get('/products');
setProducts(response.data);
} catch (err) {
setError('Failed to fetch products');
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
if (loading) {
return <p>Loading products...</p>;
}
if (error) {
return <p className={styles.error}>{error}</p>;
}
return (
<div className={styles.productsContainer}>
<h2>Products</h2>
<div className={styles.productsGrid}>
{products.map((product) => (
<div key={product._id} className={styles.productCard}>
<img src={product.imageUrl || '/placeholder.png'} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price.toFixed(2)}</p>
<Link to={`/products/${product._id}`} className={styles.detailsButton}>
View Details
</Link>
</div>
))}
</div>
</div>
);
};
export default Products;
8. Products Page Styling:
css
Copy code
/* frontend/src/pages/Products/Products.module.css */
.productsContainer {
padding: 2rem;
}
.productsContainer h2 {
text-align: center;
margin-bottom: 2rem;
}
.productsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.productCard {
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
text-align: center;
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.productCard img {
width: 100%;
height: 150px;
object-fit: cover;
border-bottom: 1px solid #dee2e6;
margin-bottom: 1rem;
}
.productCard h3 {
margin: 0.5rem 0;
}
.productCard p {
color: #28a745;
font-weight: bold;
margin-bottom: 1rem;
}
.detailsButton {
display: inline-block;
padding: 0.5rem 1rem;
background-color: #007bff;
color: #fff;
text-decoration: none;
border-radius: 0.25rem;
}
.detailsButton:hover {
background-color: #0056b3;
}
.error {
color: red;
text-align: center;
}
9. Product Detail Page:
javascript
Copy code
// frontend/src/pages/ProductDetail/ProductDetail.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
import styles from './ProductDetail.module.css';
const ProductDetail = () => {
const { id } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`/products/${id}`);
setProduct(response.data);
} catch (err) {
setError('Failed to fetch product details');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [id]);
if (loading) {
return <p>Loading product details...</p>;
}
if (error) {
return <p className={styles.error}>{error}</p>;
}
if (!product) {
return <p className={styles.error}>Product not found</p>;
}
return (
<div className={styles.productDetailContainer}>
<h2>{product.name}</h2>
<div className={styles.productDetail}>
<img src={product.imageUrl || '/placeholder.png'} alt={product.name} />
<div className={styles.productInfo}>
<p className={styles.price}>${product.price.toFixed(2)}</p>
<p>{product.description}</p>
<p>Inventory: {product.inventory}</p>
{/* Add to Cart functionality can be implemented here */}
</div>
</div>
</div>
);
};
export default ProductDetail;
10. Product Detail Page Styling:
css
Copy code
/* frontend/src/pages/ProductDetail/ProductDetail.module.css */
.productDetailContainer {
padding: 2rem;
}
.productDetailContainer h2 {
text-align: center;
margin-bottom: 2rem;
}
.productDetail {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
@media (min-width: 768px) {
.productDetail {
flex-direction: row;
align-items: flex-start;
}
}
.productDetail img {
width: 100%;
max-width: 400px;
height: auto;
object-fit: cover;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
}
.productInfo {
max-width: 600px;
}
.price {
color: #28a745;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1rem;
}
.error {
color: red;
text-align: center;
}
11. Profile Page:
javascript
Copy code
// frontend/src/pages/Profile/Profile.js
import React, { useContext, useState, useEffect } from 'react';
import axios from 'axios';
import { AuthContext } from '../../context/AuthContext';
import styles from './Profile.module.css';
const Profile = () => {
const { user } = useContext(AuthContext);
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
const fetchProfile = async () => {
try {
const response = await axios.get(`/users/${user.id}`);
setProfile(response.data);
} catch (err) {
setError('Failed to fetch profile');
} finally {
setLoading(false);
}
};
fetchProfile();
}, [user.id]);
if (loading) {
return <p>Loading profile...</p>;
}
if (error) {
return <p className={styles.error}>{error}</p>;
}
if (!profile) {
return <p className={styles.error}>Profile not found</p>;
}
return (
<div className={styles.profileContainer}>
<h2>User Profile</h2>
<div className={styles.profileDetails}>
<p>
<strong>Name:</strong> {profile.userId.name}
</p>
<p>
<strong>Email:</strong> {profile.userId.email}
</p>
<p>
<strong>Address:</strong> {profile.address || 'N/A'}
</p>
<p>
<strong>Phone:</strong> {profile.phone || 'N/A'}
</p>
{/* Add edit functionality as needed */}
</div>
</div>
);
};
export default Profile;
12. Profile Page Styling:
css
Copy code
/* frontend/src/pages/Profile/Profile.module.css */
.profileContainer {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.profileContainer h2 {
text-align: center;
margin-bottom: 1.5rem;
}
.profileDetails p {
font-size: 1.1rem;
margin-bottom: 0.75rem;
}
.error {
color: red;
text-align: center;
}
3.3.5. Setting Up API Service
1. Create api.js:
javascript
Copy code
// frontend/src/services/api.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8080', // API Gateway URL
});
// Add a request interceptor to include the JWT token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default api;
2. Update Axios Usage in Components:
Replace all instances of axios with the custom api instance to ensure JWT tokens are included automatically.
javascript
Copy code
// Example: frontend/src/pages/Products/Products.js
import api from '../../services/api';
// Then use api instead of axios
const response = await api.get('/products');
3.3.6. Handling Image Uploads and Content
1. Creating an Upload Component (Optional):
javascript
Copy code
// frontend/src/components/Upload/Upload.js
import React, { useState } from 'react';
import axios from 'axios';
import styles from './Upload.module.css';
const Upload = () => {
const [file, setFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [message, setMessage] = useState('');
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async (e) => {
e.preventDefault();
if (!file) {
setMessage('Please select a file to upload');
return;
}
const formData = new FormData();
formData.append('image', file);
try {
setUploading(true);
const response = await axios.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
setMessage('File uploaded successfully');
} catch (err) {
setMessage('File upload failed');
} finally {
setUploading(false);
}
};
return (
<div className={styles.uploadContainer}>
<h3>Upload Image</h3>
{message && <p>{message}</p>}
<form onSubmit={handleUpload} className={styles.form}>
<input type="file" onChange={handleFileChange} accept="image/*" />
<button type="submit" disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</button>
</form>
</div>
);
};
export default Upload;
2. Upload Component Styling:
css
Copy code
/* frontend/src/components/Upload/Upload.module.css */
.uploadContainer {
max-width: 400px;
margin: 2rem auto;
padding: 1.5rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
text-align: center;
}
.uploadContainer h3 {
margin-bottom: 1rem;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.form input {
width: 100%;
}
.form button {
padding: 0.5rem 1rem;
background-color: #17a2b8;
color: #fff;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.form button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
Note: The actual implementation of the /upload endpoint should be handled by the backend services. Ensure that the API Gateway routes the upload requests to the appropriate service.
3.3.7. Integrating with Backend Microservices via API Gateway
1. Setting Up Proxy for API Calls:
To avoid CORS issues and streamline API calls, set up a proxy in the React application.
1. Add Proxy to package.json:
json
Copy code
// frontend/package.json
{
// ...
"proxy": "http://localhost:8080",
// ...
}
Explanation:
proxy: Directs API calls made from the frontend to the API Gateway running on port 8080.
This allows using relative paths in API calls (e.g., /products) without specifying the full URL.
2. Making API Calls:
Utilize the api.js service for all API interactions. Here's an example of fetching products:
javascript
Copy code
// frontend/src/pages/Products/Products.js
import api from '../../services/api';
const fetchProducts = async () => {
try {
const response = await api.get('/products');
setProducts(response.data);
} catch (err) {
setError('Failed to fetch products');
} finally {
setLoading(false);
}
};
3.3.8. Managing Global State with React Context API
We've already set up AuthContext for authentication. You can extend this pattern to manage other global states, such as cart items, user preferences, etc.
Example: Cart Context
1. Create CartContext.js:
javascript
Copy code
// frontend/src/context/CartContext.js
import React, { createContext, useState } from 'react';
export const CartContext = createContext();
const CartProvider = ({ children }) => {
const [cartItems, setCartItems] = useState([]);
const addToCart = (product) => {
setCartItems((prevItems) => [...prevItems, product]);
};
const removeFromCart = (productId) => {
setCartItems((prevItems) => prevItems.filter(item => item._id !== productId));
};
const clearCart = () => {
setCartItems([]);
};
return (
<CartContext.Provider value={{ cartItems, addToCart, removeFromCart, clearCart }}>
{children}
</CartContext.Provider>
);
};
export default CartProvider;
2. Wrap App.js with CartProvider:
javascript
Copy code
// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import AuthProvider from './context/AuthContext';
import CartProvider from './context/CartContext';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<CartProvider>
<App />
</CartProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
3.3.9. Styling and Responsiveness
1. Import Bootstrap (Optional):
For a responsive design and pre-built UI components, integrate Bootstrap.
bash
Copy code
npm install bootstrap
2. Import Bootstrap in index.js:
javascript
Copy code
// frontend/src/index.js
import 'bootstrap/dist/css/bootstrap.min.css';
3. Utilize Bootstrap Classes:
Enhance components using Bootstrap's grid system and utility classes.
javascript
Copy code
// Example: frontend/src/pages/Home/Home.js
import React from 'react';
import styles from './Home.module.css';
const Home = () => {
return (
<div className={`container ${styles.home}`}>
<h1 className="text-center">Welcome to iBranDIDit.com</h1>
<p className="text-center">Your one-stop shop for all your needs.</p>
</div>
);
};
export default Home;
Note: You can mix custom CSS modules with Bootstrap classes to achieve the desired look and feel.
________________________________________
4. Complete Code and Configuration Files
4.1. Root Directory Files
ibrandidit-microservices/package.json
(Updated in Stage 9)
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service",
"frontend"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\"",
"frontend": "npm --workspace frontend start"
},
"dependencies": {},
"devDependencies": {}
}
Explanation:
• workspaces: Now includes frontend.
• scripts: Added a shortcut to start the frontend separately if needed.
ibrandidit-microservices/README.md
(Already provided in Stage 8)
________________________________________
4.2. Frontend Directory Files
frontend/package.json
(Generated by Create React App and updated in Stage 9)
json
Copy code
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.4.0",
"bootstrap": "^5.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"docs": "jsdoc -c jsdoc.json",
"lint": "eslint . --ext .js",
"format": "prettier --write ."
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:8080"
}
Explanation:
• Dependencies: Added axios, bootstrap, react-router-dom for API calls, styling, and routing.
• Scripts: Included scripts for linting and formatting using ESLint and Prettier.
frontend/.gitignore
(Already provided in Stage 9)
gitignore
Copy code
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
frontend/README.md
markdown
Copy code
# iBranDIDit.com Frontend
## Overview
The frontend application for iBranDIDit.com is built using React, providing a responsive and user-friendly interface for customers. It interacts with the backend microservices via the API Gateway, handling user authentication, product browsing, profile management, and more.
## Table of Contents
- [Project Structure](#project-structure)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Available Scripts](#available-scripts)
- [Routing](#routing)
- [State Management](#state-management)
- [Styling](#styling)
- [API Integration](#api-integration)
- [Testing](#testing)
- [Deployment](#deployment)
- [Contributing](#contributing)
- [License](#license)
## Project Structure
frontend/ ├── public/ │ ├── index.html │ └── ... ├── src/ │ ├── components/ │ │ ├── Navbar/ │ │ ├── Footer/ │ │ └── ... (other reusable components) │ ├── pages/ │ │ ├── Home/ │ │ ├── Login/ │ │ ├── Register/ │ │ ├── Products/ │ │ ├── ProductDetail/ │ │ ├── Profile/ │ │ └── ... (other pages) │ ├── context/ │ │ ├── AuthContext.js │ │ └── ... (other contexts) │ ├── services/ │ │ ├── api.js │ │ └── ... (other services) │ ├── App.js │ ├── index.js │ └── ... (other configurations) ├── .gitignore ├── package.json └── README.md
markdown
Copy code
## Getting Started
### Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
### Installation
1. **Navigate to the Frontend Directory:**
```bash
cd frontend
2. Install Dependencies:
bash
Copy code
npm install
3. Configure Environment Variables:
Create a .env file in the frontend/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:8080
Descriptions:
o REACT_APP_API_BASE_URL: Base URL for the API Gateway.
Note: Environment variables prefixed with REACT_APP_ are accessible in the React application.
4. Start the Development Server:
bash
Copy code
npm start
Note: The app will run on http://localhost:3000 by default.
Available Scripts
In the frontend/ directory, you can run:
• npm start: Runs the app in development mode.
• npm run build: Builds the app for production.
• npm test: Launches the test runner.
• npm run eject: Ejects the app from Create React App configuration (irreversible).
• npm run docs: Generates code documentation using JSDoc.
• npm run lint: Runs ESLint to analyze code for potential errors.
• npm run format: Formats code using Prettier.
Routing
Routing is managed using React Router. The primary routes include:
• /: Home page.
• /login: User login.
• /register: User registration.
• /products: Product listings.
• /products/:id: Product detail page.
• /profile: User profile (protected route).
State Management
Global state is managed using the React Context API, specifically:
• AuthContext: Manages authentication state, including user data and JWT tokens.
• CartContext (optional): Manages shopping cart items.
Styling
Styling is handled using:
• CSS Modules: Scoped CSS for individual components to prevent style clashes.
• Bootstrap: Integrated for responsive design and pre-built UI components.
API Integration
API interactions are managed using Axios through a centralized api.js service, which includes interceptors to attach JWT tokens automatically.
Example: Fetching Products
javascript
Copy code
import api from '../services/api';
const fetchProducts = async () => {
try {
const response = await api.get('/products');
setProducts(response.data);
} catch (error) {
console.error('Failed to fetch products:', error);
}
};
Testing
Testing is conducted using Jest and React Testing Library.
1. Run Tests:
bash
Copy code
npm test
2. Writing Tests:
Create test files alongside components (e.g., Navbar.test.js for Navbar.js) and use React Testing Library to write unit and integration tests.
Example: Navbar Test
javascript
Copy code
// frontend/src/components/Navbar/Navbar.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Navbar from './Navbar';
import { AuthContext } from '../../context/AuthContext';
test('renders Navbar with login and register links when not authenticated', () => {
render(
<AuthContext.Provider value={{ isAuthenticated: false }}>
<BrowserRouter>
<Navbar />
</BrowserRouter>
</AuthContext.Provider>
);
expect(screen.getByText(/Login/i)).toBeInTheDocument();
expect(screen.getByText(/Register/i)).toBeInTheDocument();
expect(screen.queryByText(/Logout/i)).not.toBeInTheDocument();
});
test('renders Navbar with profile and logout links when authenticated', () => {
render(
<AuthContext.Provider value={{ isAuthenticated: true, user: { name: 'Test User' } }}>
<BrowserRouter>
<Navbar />
</BrowserRouter>
</AuthContext.Provider>
);
expect(screen.getByText(/Profile/i)).toBeInTheDocument();
expect(screen.getByText(/Logout/i)).toBeInTheDocument();
expect(screen.queryByText(/Login/i)).not.toBeInTheDocument();
});
5. Documentation and Usage
5.1. Running the Frontend Locally
1. Ensure Backend Services Are Running:
o Start all backend microservices and the API Gateway using Docker Compose.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
2. Start the Frontend Development Server:
bash
Copy code
cd frontend
npm start
o The app will be accessible at http://localhost:3000.
5.2. Building for Production
1. Create a Production Build:
bash
Copy code
cd frontend
npm run build
2. Serve the Build:
o You can serve the build using a static server or integrate it with your backend deployment process.
bash
Copy code
npm install -g serve
serve -s build
o The app will be accessible at http://localhost:5000 (default port for serve).
5.3. Deployment
1. Choose a Hosting Platform:
o Netlify
o Vercel
o GitHub Pages
o AWS Amplify
o Heroku
2. Deploying to Netlify (Example):
o Connect Repository:
Push your frontend code to GitHub.
Log in to Netlify and select "New site from Git."
Choose your GitHub repository and branch.
o Configure Build Settings:
Build Command: npm run build
Publish Directory: frontend/build
o Set Environment Variables:
Add REACT_APP_API_BASE_URL with the production API Gateway URL.
o Deploy:
Click "Deploy site." Netlify will build and host your frontend.
5.4. Updating Content and Images
1. Adding Images:
o Place images in the public/ directory or fetch them from the backend if dynamic uploads are supported.
o Reference images using relative paths.
javascript
Copy code
// Example: frontend/src/pages/Home/Home.js
<img src="/images/banner.jpg" alt="Banner" />
2. Updating Content:
o Modify the relevant components or pages in the src/pages/ or src/components/ directories.
o Use Markdown files or CMS integrations for dynamic content (optional for future stages).
5.5. Generating Documentation
1. Annotate Components with JSDoc:
Add JSDoc comments to your components and functions to facilitate documentation generation.
javascript
Copy code
// frontend/src/components/Navbar/Navbar.js
/**
* Navbar Component
*
* Displays the navigation bar with links to different pages.
*
* @returns {JSX.Element} The Navbar component.
*/
const Navbar = () => {
// Component logic
};
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
5.6. Contribution Guidelines
1. Follow the Root CONTRIBUTING.md:
Ensure that all frontend contributors adhere to the established contribution guidelines.
2. Branching Strategy:
o Feature Branches: Create a new branch for each feature or bug fix.
o Pull Requests: Submit pull requests for code reviews before merging into main.
3. Coding Standards:
o Linting: Run npm run lint to check for code issues.
o Formatting: Run npm run format to ensure consistent code formatting.
________________________________________
6. Complete Frontend Code and Configuration Files
6.1. Frontend package.json
(Already provided in section 4.2.2)
6.2. Frontend Directory Structure
java
Copy code
frontend/
├── public/
│ ├── index.html
│ ├── favicon.ico
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── robots.txt
│ └── ... (other public assets)
├── src/
│ ├── components/
│ │ ├── Navbar/
│ │ │ ├── Navbar.js
│ │ │ ├── Navbar.module.css
│ │ │ └── Navbar.test.js
│ │ ├── Footer/
│ │ │ ├── Footer.js
│ │ │ ├── Footer.module.css
│ │ │ └── Footer.test.js
│ │ ├── ProtectedRoute/
│ │ │ ├── ProtectedRoute.js
│ │ │ ├── ProtectedRoute.module.css
│ │ │ └── ProtectedRoute.test.js
│ │ └── ... (other reusable components)
│ ├── pages/
│ │ ├── Home/
│ │ │ ├── Home.js
│ │ │ ├── Home.module.css
│ │ │ └── Home.test.js
│ │ ├── Login/
│ │ │ ├── Login.js
│ │ │ ├── Login.module.css
│ │ │ └── Login.test.js
│ │ ├── Register/
│ │ │ ├── Register.js
│ │ │ ├── Register.module.css
│ │ │ └── Register.test.js
│ │ ├── Products/
│ │ │ ├── Products.js
│ │ │ ├── Products.module.css
│ │ │ └── Products.test.js
│ │ ├── ProductDetail/
│ │ │ ├── ProductDetail.js
│ │ │ ├── ProductDetail.module.css
│ │ │ └── ProductDetail.test.js
│ │ ├── Profile/
│ │ │ ├── Profile.js
│ │ │ ├── Profile.module.css
│ │ │ └── Profile.test.js
│ │ └── ... (other pages)
│ ├── context/
│ │ ├── AuthContext.js
│ │ ├── AuthContext.test.js
│ │ ├── CartContext.js
│ │ └── CartContext.test.js
│ ├── services/
│ │ ├── api.js
│ │ └── auth.js
│ ├── App.js
│ ├── App.css
│ ├── index.js
│ ├── index.css
│ ├── setupTests.js
│ └── ... (other configurations)
├── .gitignore
├── package.json
├── README.md
├── jsdoc.json
└── ... (other configuration files)
6.3. Sample Component Files
frontend/src/App.js
javascript
Copy code
// frontend/src/App.js
import React, { useContext } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar/Navbar';
import Footer from './components/Footer/Footer';
import Home from './pages/Home/Home';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Products from './pages/Products/Products';
import ProductDetail from './pages/ProductDetail/ProductDetail';
import Profile from './pages/Profile/Profile';
import ProtectedRoute from './components/ProtectedRoute/ProtectedRoute';
import { AuthContext } from './context/AuthContext';
import './App.css';
function App() {
const { loading } = useContext(AuthContext);
if (loading) {
return <p>Loading...</p>;
}
return (
<Router>
<Navbar />
<div className="content">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
{/* Add more routes as needed */}
</Routes>
</div>
<Footer />
</Router>
);
}
export default App;
frontend/src/index.js
(Already provided in section 3.3.2)
javascript
Copy code
// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import AuthProvider from './context/AuthContext';
import CartProvider from './context/CartContext';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<CartProvider>
<App />
</CartProvider>
</AuthProvider>
</React.StrictMode>,
document.getElementById('root')
);
frontend/src/context/AuthContext.js
(Already provided in section 3.3.2)
frontend/src/services/api.js
(Already provided in section 3.3.7)
javascript
Copy code
// frontend/src/services/api.js
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080',
});
// Add a request interceptor to include the JWT token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default api;
frontend/src/pages/Login/Login.js
(Already provided in section 3.3.3)
frontend/src/pages/Register/Register.js
(Already provided in section 3.3.3)
frontend/src/pages/Products/Products.js
(Already provided in section 3.3.3)
frontend/src/pages/ProductDetail/ProductDetail.js
(Already provided in section 3.3.3)
frontend/src/pages/Profile/Profile.js
(Already provided in section 3.3.3)
frontend/src/components/Navbar/Navbar.js
(Already provided in section 3.3.3)
frontend/src/components/Footer/Footer.js
(Already provided in section 3.3.3)
frontend/src/components/ProtectedRoute/ProtectedRoute.js
(Already provided in section 3.3.3)
________________________________________
7. Documentation and Usage
7.1. Running the Frontend Locally
1. Ensure Backend Services Are Running:
o Start all backend microservices and the API Gateway using Docker Compose.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
2. Start the Frontend Development Server:
bash
Copy code
cd frontend
npm start
o The app will be accessible at http://localhost:3000.
7.2. Building for Production
1. Create a Production Build:
bash
Copy code
cd frontend
npm run build
2. Serve the Build:
o You can serve the build using a static server or integrate it with your backend deployment process.
bash
Copy code
npm install -g serve
serve -s build
o The app will be accessible at http://localhost:5000 (default port for serve).
7.3. Deployment
1. Choose a Hosting Platform:
o Netlify
o Vercel
o GitHub Pages
o AWS Amplify
o Heroku
2. Deploying to Netlify (Example):
o Connect Repository:
Push your frontend code to GitHub.
Log in to Netlify and select "New site from Git."
Choose your GitHub repository and branch.
o Configure Build Settings:
Build Command: npm run build
Publish Directory: frontend/build
o Set Environment Variables:
Add REACT_APP_API_BASE_URL with the production API Gateway URL.
o Deploy:
Click "Deploy site." Netlify will build and host your frontend.
7.4. Updating Content and Images
1. Adding Images:
o Place images in the public/ directory or fetch them from the backend if dynamic uploads are supported.
o Reference images using relative paths.
javascript
Copy code
// Example: frontend/src/pages/Home/Home.js
<img src="/images/banner.jpg" alt="Banner" />
2. Updating Content:
o Modify the relevant components or pages in the src/pages/ or src/components/ directories.
o Use Markdown files or CMS integrations for dynamic content (optional for future stages).
7.5. Generating Documentation
1. Annotate Components with JSDoc:
Add JSDoc comments to your components and functions to facilitate documentation generation.
javascript
Copy code
// frontend/src/components/Navbar/Navbar.js
/**
* Navbar Component
*
* Displays the navigation bar with links to different pages.
*
* @returns {JSX.Element} The Navbar component.
*/
const Navbar = () => {
// Component logic
};
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
7.6. Contribution Guidelines
1. Follow the Root CONTRIBUTING.md:
Ensure that all frontend contributors adhere to the established contribution guidelines.
2. Branching Strategy:
o Feature Branches: Create a new branch for each feature or bug fix.
o Pull Requests: Submit pull requests for code reviews before merging into main.
3. Coding Standards:
o Linting: Run npm run lint to check for code issues.
o Formatting: Run npm run format to ensure consistent code formatting.
________________________________________
8. Troubleshooting Tips
1. CORS Issues:
o Problem: API calls from frontend are blocked due to CORS policy.
o Solution: Ensure that the API Gateway has proper CORS configurations to allow requests from the frontend origin (http://localhost:3000).
2. Authentication Errors:
o Problem: Frontend receives 401 or 403 errors when making authenticated API calls.
o Solution:
Ensure that the JWT token is correctly stored in localStorage.
Verify that the token is included in the Authorization header via Axios interceptors.
Confirm that the backend services correctly validate the token.
3. Proxy Not Working:
o Problem: API calls are not being proxied correctly, leading to network errors.
o Solution:
Ensure that the proxy field in frontend/package.json is correctly set to the API Gateway URL.
Restart the frontend development server after changing the proxy settings.
4. Deployment Issues:
o Problem: The deployed frontend does not function as expected.
o Solution:
Check environment variables and ensure REACT_APP_API_BASE_URL points to the correct production API Gateway URL.
Verify that all build steps completed successfully.
Review hosting platform logs for specific error messages.
5. Component Styling Not Applying:
o Problem: CSS Modules are not applying styles to components.
o Solution:
Ensure that CSS module files are correctly named with the .module.css extension.
Verify that styles are imported and used correctly in components.
6. Missing Images or Assets:
o Problem: Images or other assets are not loading.
o Solution:
Ensure that assets are placed in the public/ directory or correctly referenced from the backend.
Check the paths used in src attributes of <img> tags.
7. Unhandled Promise Rejections:
o Problem: API calls result in unhandled promise rejections, causing the app to crash.
o Solution:
Ensure that all asynchronous operations are properly handled using try-catch blocks.
Provide user feedback in case of errors.
________________________________________
9. Additional Resources
• React Documentation: https://reactjs.org/docs/getting-started.html
• React Router Documentation: https://reactrouter.com/docs/en/v6
• Axios Documentation: https://axios-http.com/docs/intro
• Bootstrap Documentation: https://getbootstrap.com/docs/5.3/getting-started/introduction/
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library: https://testing-library.com/docs/react-testing-library/intro/
• JSDoc Documentation: https://jsdoc.app/
• Create React App Documentation: https://create-react-app.dev/docs/getting-started/
• NPM Workspaces: https://docs.npmjs.com/cli/v7/using-npm/workspaces
________________________________________
Disclaimer: This guide provides a foundational approach to building a modular frontend for your microservices-based eCommerce platform. Depending on project complexity and specific requirements, further optimizations and advanced features may be necessary, such as integrating state management libraries like Redux, implementing server-side rendering with Next.js, or enhancing accessibility and performance.
________________________________________
By following this comprehensive guide for Stage 9: Modular Frontend Development, you've established a robust frontend application that interacts seamlessly with your backend microservices. This setup not only enhances the user experience but also ensures that your codebase remains organized, scalable, and maintainable as your project evolves.
Proceeding to Stage 10: Responsive Design and Accessibility will further refine your frontend, ensuring that it caters to a wide range of devices and users, adhering to best practices in web development.
Stage 10: Backend Development is a pivotal step in building a robust and scalable backend infrastructure for your eCommerce platform, iBranDIDit.com. This stage focuses on developing a new Order Service, which is essential for handling customer orders, managing order data, and integrating seamlessly with existing microservices like Auth Service, User Service, and Product Service.
This guide will provide comprehensive instructions, complete with code snippets, dependencies, configuration files, and documentation to ensure easy integration. Designed with beginners in mind, each step builds upon previous stages to maintain consistency and functionality across the entire system.
________________________________________
Stage 10: Backend Development - Order Service
________________________________________
1. Overview
Objective:
• Develop the Order Service: Implement a microservice responsible for handling customer orders, including order creation, retrieval, and management.
• Integrate with Existing Services: Ensure seamless communication with Auth, User, and Product services via the API Gateway and message broker (RabbitMQ).
• Implement Event-Driven Architecture: Publish events to RabbitMQ for actions like order creation, enabling other services (e.g., Notification Service) to react accordingly.
• Ensure Scalability and Maintainability: Build the service following best practices to facilitate future enhancements and scalability.
Benefits:
• Centralized Order Management: Provides a dedicated service for managing all order-related operations.
• Decoupled Architecture: Promotes separation of concerns, making the system easier to maintain and scale.
• Real-Time Notifications: Enables other services to respond to order events in real-time, enhancing user experience.
________________________________________
2. Technology Stack
• Runtime Environment: Node.js v14 or higher
• Framework: Express.js – For building the RESTful API
• Database: MongoDB – For storing order data
• ODM: Mongoose – For MongoDB object modeling
• Message Broker: RabbitMQ – For event-driven communication
• Authentication: JWT – For securing API endpoints
• Logging: Winston – For logging application events
• Configuration Management: dotenv – For managing environment variables
• Testing: Jest and Supertest – For unit and integration testing
• Version Control: Git – Continuation from Stage 8
• Documentation: Markdown and Swagger – For API documentation
________________________________________
3. Implementation Steps
3.1. Setting Up the Order Service Directory
1. Navigate to the Root Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Create the Order Service Workspace:
We'll use NPM Workspaces to manage the Order Service alongside existing microservices.
bash
Copy code
mkdir order-service
3. Initialize a New Node.js Project:
bash
Copy code
cd order-service
npm init -y
4. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv jsonwebtoken amqplib winston
5. Install Development Dependencies:
bash
Copy code
npm install --save-dev nodemon jest supertest eslint prettier
6. Update Root package.json:
Add order-service to the workspaces array.
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service",
"frontend",
"order-service"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\"",
"frontend": "npm --workspace frontend start",
"order-service": "npm --workspace order-service start"
},
"dependencies": {},
"devDependencies": {}
}
3.2. Project Structure Overview
The order-service/ directory will have the following structure:
css
Copy code
order-service/
├── controllers/
│ └── orderController.js
├── models/
│ └── Order.js
├── routes/
│ └── orderRoutes.js
├── services/
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ ├── order.test.js
│ └── orderIntegration.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
└── index.js
Explanation:
• controllers/: Contains business logic for handling requests.
• models/: Defines the Mongoose schema for orders.
• routes/: Defines API endpoints and associates them with controllers.
• services/: Handles external services like RabbitMQ.
• middleware/: Contains middleware for authentication and error handling.
• config/: Manages configuration, such as database connections.
• tests/: Contains unit and integration tests.
• .env.example: Template for environment variables.
• .gitignore: Specifies files and directories to ignore in Git.
• jest.config.js: Configuration for Jest testing framework.
• README.md: Documentation for the Order Service.
• index.js: Entry point of the application.
3.3. Developing the Order Service Components
3.3.1. Configuration Files
1. .gitignore
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# OS files
.DS_Store
Thumbs.db
2. .env.example
env
Copy code
PORT=5004
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Descriptions:
o PORT: Port on which the Order Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
3. jest.config.js
javascript
Copy code
// order-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js',
'services/**/*.js'
]
};
4. jest.setup.js
(Create this file in the root of order-service/)
javascript
Copy code
// order-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
3.3.2. Database Configuration
1. config/db.js
javascript
Copy code
// order-service/config/db.js
const mongoose = require('mongoose');
const winston = require('winston');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
winston.info('Order Service connected to MongoDB');
} catch (error) {
winston.error('Order Service failed to connect to MongoDB:', error);
process.exit(1);
}
};
module.exports = connectDB;
3.3.3. Models
1. models/Order.js
javascript
Copy code
// order-service/models/Order.js
const mongoose = require('mongoose');
const OrderSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'User', // Reference to User model in User Service
},
products: [
{
productId: {
type: String,
required: true,
ref: 'Product', // Reference to Product model in Product Service
},
quantity: {
type: Number,
required: true,
min: 1,
},
price: {
type: Number,
required: true,
min: 0,
},
},
],
totalAmount: {
type: Number,
required: true,
min: 0,
},
status: {
type: String,
enum: ['Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled'],
default: 'Pending',
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
OrderSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Order', OrderSchema);
3.3.4. Controllers
1. controllers/orderController.js
javascript
Copy code
// order-service/controllers/orderController.js
const Order = require('../models/Order');
const rabbitmqService = require('../services/rabbitmqService');
const winston = require('winston');
/**
* Create a new order
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const createOrder = async (req, res) => {
try {
const { userId, products } = req.body;
if (!userId || !products || !Array.isArray(products) || products.length === 0) {
return res.status(400).json({ message: 'Invalid order data' });
}
// Calculate total amount
const totalAmount = products.reduce((acc, item) => acc + item.price * item.quantity, 0);
const newOrder = new Order({
userId,
products,
totalAmount,
});
const savedOrder = await newOrder.save();
// Publish order created event to RabbitMQ
await rabbitmqService.publishEvent('order_created', {
id: savedOrder._id,
userId: savedOrder.userId,
products: savedOrder.products,
totalAmount: savedOrder.totalAmount,
});
res.status(201).json(savedOrder);
} catch (error) {
winston.error('Error creating order:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Get all orders
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getAllOrders = async (req, res) => {
try {
const orders = await Order.find();
res.status(200).json(orders);
} catch (error) {
winston.error('Error fetching orders:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Get order by ID
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getOrderById = async (req, res) => {
try {
const { id } = req.params;
const order = await Order.findById(id);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
res.status(200).json(order);
} catch (error) {
winston.error('Error fetching order:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Update order status
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const updateOrderStatus = async (req, res) => {
try {
const { id } = req.params;
const { status } = req.body;
const validStatuses = ['Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled'];
if (!validStatuses.includes(status)) {
return res.status(400).json({ message: 'Invalid status value' });
}
const order = await Order.findById(id);
if (!order) {
return res.status(404).json({ message: 'Order not found' });
}
order.status = status;
const updatedOrder = await order.save();
// Publish order status updated event to RabbitMQ
await rabbitmqService.publishEvent('order_status_updated', {
id: updatedOrder._id,
status: updatedOrder.status,
});
res.status(200).json(updatedOrder);
} catch (error) {
winston.error('Error updating order status:', error);
res.status(500).json({ message: 'Server error' });
}
};
module.exports = {
createOrder,
getAllOrders,
getOrderById,
updateOrderStatus,
};
3.3.5. Routes
1. routes/orderRoutes.js
javascript
Copy code
// order-service/routes/orderRoutes.js
const express = require('express');
const router = express.Router();
const orderController = require('../controllers/orderController');
const authMiddleware = require('../middleware/authMiddleware');
/**
* @route POST /orders
* @desc Create a new order
* @access Protected
*/
router.post('/', authMiddleware, orderController.createOrder);
/**
* @route GET /orders
* @desc Get all orders
* @access Protected
*/
router.get('/', authMiddleware, orderController.getAllOrders);
/**
* @route GET /orders/:id
* @desc Get order by ID
* @access Protected
*/
router.get('/:id', authMiddleware, orderController.getOrderById);
/**
* @route PUT /orders/:id/status
* @desc Update order status
* @access Protected
*/
router.put('/:id/status', authMiddleware, orderController.updateOrderStatus);
module.exports = router;
3.3.6. Middleware
1. middleware/authMiddleware.js
javascript
Copy code
// order-service/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const winston = require('winston');
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authorization token missing or malformed' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
winston.error('Invalid token:', error);
return res.status(401).json({ message: 'Invalid or expired token' });
}
};
module.exports = authMiddleware;
2. middleware/errorHandler.js
javascript
Copy code
// order-service/middleware/errorHandler.js
const winston = require('winston');
const errorHandler = (err, req, res, next) => {
winston.error(err.message, err);
res.status(500).json({ message: 'Server error' });
};
module.exports = errorHandler;
3.3.7. Services
1. services/rabbitmqService.js
javascript
Copy code
// order-service/services/rabbitmqService.js
const amqp = require('amqplib');
const winston = require('winston');
let channel = null;
const connectRabbitMQ = async () => {
try {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
winston.info('Connected to RabbitMQ');
} catch (error) {
winston.error('Failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const publishEvent = async (event, data) => {
if (!channel) {
await connectRabbitMQ();
}
const queue = 'order_events';
await channel.assertQueue(queue, { durable: true });
const payload = { event, data };
channel.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
winston.info(`Published event to RabbitMQ: ${event}`);
};
connectRabbitMQ();
module.exports = {
publishEvent,
};
Explanation:
o connectRabbitMQ: Establishes a connection to RabbitMQ and creates a channel.
o publishEvent: Publishes events to the specified queue (order_events).
3.3.8. Controllers and Routes
(Already covered in Controllers and Routes sections above.)
3.3.9. Entry Point
1. index.js
javascript
Copy code
// order-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const bodyParser = require('body-parser');
const winston = require('winston');
const connectDB = require('./config/db');
const orderRoutes = require('./routes/orderRoutes');
const errorHandler = require('./middleware/errorHandler');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Logging configuration
winston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
// Connect to MongoDB
connectDB();
// Routes
app.use('/orders', orderRoutes);
// Error Handling Middleware
app.use(errorHandler);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Order Service API');
});
// Start Server
const PORT = process.env.PORT || 5004;
app.listen(PORT, () => {
winston.info(`Order Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3.3.10. Testing
1. Unit Tests
o tests/order.test.js
javascript
Copy code
// order-service/tests/order.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../index');
const Order = require('../models/Order');
const jwt = require('jsonwebtoken');
// Generate a test JWT token
const testUser = {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(testUser, process.env.JWT_SECRET, { expiresIn: '1h' });
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/order_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await Order.deleteMany({});
await mongoose.connection.close();
});
describe('Order API Endpoints', () => {
it('should create a new order', async () => {
const res = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUser.id,
products: [
{
productId: '60d5fb0ab1e8e1d4c8e4f8c7',
quantity: 2,
price: 49.99,
},
],
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUser.id);
expect(res.body.products.length).toBe(1);
expect(res.body).toHaveProperty('totalAmount', 99.98);
});
it('should not create an order with invalid data', async () => {
const res = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${token}`)
.send({
userId: '',
products: [],
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid order data');
});
it('should retrieve all orders', async () => {
const res = await request(app)
.get('/orders')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBeGreaterThan(0);
});
it('should retrieve an order by ID', async () => {
const order = await Order.findOne();
const res = await request(app)
.get(`/orders/${order._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('_id', order._id.toString());
});
it('should update order status', async () => {
const order = await Order.findOne();
const res = await request(app)
.put(`/orders/${order._id}/status`)
.set('Authorization', `Bearer ${token}`)
.send({ status: 'Confirmed' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('status', 'Confirmed');
});
it('should not update order status with invalid status', async () => {
const order = await Order.findOne();
const res = await request(app)
.put(`/orders/${order._id}/status`)
.set('Authorization', `Bearer ${token}`)
.send({ status: 'UnknownStatus' });
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid status value');
});
it('should return 404 for non-existent order', async () => {
const res = await request(app)
.get('/orders/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Order not found');
});
});
2. Integration Tests
o tests/orderIntegration.test.js
javascript
Copy code
// order-service/tests/orderIntegration.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../index');
const Order = require('../models/Order');
const jwt = require('jsonwebtoken');
let mongoServer;
let token;
const testUser = {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Integration Test User',
email: 'integration@example.com',
};
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Connect to in-memory MongoDB
await mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// Generate a test JWT token
token = jwt.sign(testUser, process.env.JWT_SECRET, { expiresIn: '1h' });
});
afterAll(async () => {
// Close connection and stop server
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongoServer.stop();
});
describe('Order Service Integration Tests', () => {
it('should create a new order and publish event', async () => {
const res = await request(app)
.post('/orders')
.set('Authorization', `Bearer ${token}`)
.send({
userId: testUser.id,
products: [
{
productId: '60d5fb0ab1e8e1d4c8e4f8c7',
quantity: 3,
price: 29.99,
},
],
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('userId', testUser.id);
expect(res.body.products.length).toBe(1);
expect(res.body).toHaveProperty('totalAmount', 89.97);
// Optionally, verify that the event was published to RabbitMQ
// This requires mocking or spying on the rabbitmqService.publishEvent method
// For simplicity, this step can be skipped or handled with more advanced setups
});
it('should retrieve all orders', async () => {
const res = await request(app)
.get('/orders')
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(Array.isArray(res.body)).toBeTruthy();
expect(res.body.length).toBe(1);
});
});
3.3.11. Logging Configuration
The service uses Winston for logging. Configuration is included in index.js and other parts as needed.
3.3.12. API Documentation
1. Install Swagger Dependencies:
bash
Copy code
npm install swagger-jsdoc swagger-ui-express
2. Create swagger.js:
javascript
Copy code
// order-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Order Service API',
version: '1.0.0',
description: 'API documentation for the Order Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5004',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
OrderInput: {
type: 'object',
required: ['userId', 'products'],
properties: {
userId: {
type: 'string',
description: 'User ID referencing the User model',
},
products: {
type: 'array',
items: {
type: 'object',
properties: {
productId: {
type: 'string',
description: 'Product ID referencing the Product model',
},
quantity: {
type: 'number',
description: 'Quantity of the product',
minimum: 1,
},
price: {
type: 'number',
description: 'Price of the product at the time of order',
minimum: 0,
},
},
required: ['productId', 'quantity', 'price'],
},
},
},
example: {
userId: '60d5f9f9b1e8e1d4c8e4f8c5',
products: [
{
productId: '60d5fb0ab1e8e1d4c8e4f8c7',
quantity: 2,
price: 49.99,
},
],
},
},
OrderOutput: {
type: 'object',
properties: {
_id: {
type: 'string',
description: 'Order ID',
},
userId: {
type: 'string',
description: 'User ID',
},
products: {
type: 'array',
items: {
type: 'object',
properties: {
productId: {
type: 'string',
description: 'Product ID',
},
quantity: {
type: 'number',
description: 'Quantity of the product',
},
price: {
type: 'number',
description: 'Price of the product',
},
},
},
},
totalAmount: {
type: 'number',
description: 'Total amount for the order',
},
status: {
type: 'string',
enum: ['Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled'],
description: 'Current status of the order',
},
createdAt: {
type: 'string',
format: 'date-time',
description: 'Order creation timestamp',
},
updatedAt: {
type: 'string',
format: 'date-time',
description: 'Order update timestamp',
},
},
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
3. Integrate Swagger in index.js:
javascript
Copy code
// order-service/index.js
// ... previous imports
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
// ... existing middleware and routes
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// ... rest of the code
4. Annotate Routes with Swagger Comments:
javascript
Copy code
// order-service/routes/orderRoutes.js
/**
* @swagger
* tags:
* name: Orders
* description: Order management
*/
/**
* @swagger
* /orders:
* post:
* summary: Create a new order
* tags: [Orders]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/OrderInput'
* responses:
* 201:
* description: Order created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/OrderOutput'
* 400:
* description: Invalid order data
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.post('/', authMiddleware, orderController.createOrder);
/**
* @swagger
* /orders:
* get:
* summary: Get all orders
* tags: [Orders]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: A list of orders
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/OrderOutput'
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.get('/', authMiddleware, orderController.getAllOrders);
/**
* @swagger
* /orders/{id}:
* get:
* summary: Get order by ID
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: string
* required: true
* description: Order ID
* responses:
* 200:
* description: Order details
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/OrderOutput'
* 401:
* description: Unauthorized
* 404:
* description: Order not found
* 500:
* description: Server error
*/
router.get('/:id', authMiddleware, orderController.getOrderById);
/**
* @swagger
* /orders/{id}/status:
* put:
* summary: Update order status
* tags: [Orders]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: string
* required: true
* description: Order ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* enum: ['Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled']
* required:
* - status
* example:
* status: 'Confirmed'
* responses:
* 200:
* description: Order status updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/OrderOutput'
* 400:
* description: Invalid status value
* 401:
* description: Unauthorized
* 404:
* description: Order not found
* 500:
* description: Server error
*/
router.put('/:id/status', authMiddleware, orderController.updateOrderStatus);
3.3.13. Testing Configuration
1. Update package.json Scripts:
json
Copy code
// order-service/package.json
{
// ...
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
// ...
}
2. Create jsdoc.json:
json
Copy code
// order-service/jsdoc.json
{
"source": {
"include": ["./routes", "./controllers", "./models", "./middleware"],
"exclude": ["node_modules"],
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
}
}
}
3.3.14. README Documentation
1. README.md
markdown
Copy code
# Order Service
## Overview
The **Order Service** is a microservice responsible for managing customer orders in the iBranDIDit.com eCommerce platform. It handles order creation, retrieval, and status updates, integrating seamlessly with other services like Auth Service, User Service, and Product Service via the API Gateway and RabbitMQ.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Logging](#logging)
- [Contributing](#contributing)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [MongoDB](https://www.mongodb.com/) (local or hosted)
- [RabbitMQ](https://www.rabbitmq.com/) (local or hosted)
- [NPM](https://www.npmjs.com/) (comes with Node.js)
## Installation
1. **Navigate to the Order Service Directory:**
```bash
cd order-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the order-service/ directory based on the provided .env.example.
env
Copy code
PORT=5004
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Descriptions:
o PORT: Port on which the Order Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
3. Start the Service:
bash
Copy code
docker-compose up --build order-service
Note: Ensure that MongoDB and RabbitMQ services are also running, either via the root docker-compose.yml or individually.
Option 2: Running Locally
4. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
5. Start the Order Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
o URL: http://localhost:5004/api-docs
Testing
Run tests using Jest:
bash
Copy code
npm test
Coverage Reports:
After running tests, view coverage reports in the coverage/ directory.
Logging
The Order Service uses Winston for logging. Logs are output to the console in a simple format. You can configure additional transports (e.g., file, external logging services) by modifying the Winston configuration in index.js.
Contributing
Contributions are welcome! Please adhere to the following guidelines:
6. Fork the Repository: Create a personal fork of the Order Service repository.
7. Create a Feature Branch: Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/your-feature-name
8. Commit Changes: Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(order-service): add order creation endpoint"
9. Push to Fork: Push your changes to your forked repository.
bash
Copy code
git push origin feature/your-feature-name
10. Create a Pull Request: Submit a pull request to the main repository for review.
License
MIT
________________________________________
Copy code
3.4. Creating the Order Service
Now, let's create each file as per the project structure.
3.4.1. index.js
javascript
Copy code
// order-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const bodyParser = require('body-parser');
const winston = require('winston');
const connectDB = require('./config/db');
const orderRoutes = require('./routes/orderRoutes');
const errorHandler = require('./middleware/errorHandler');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Logging configuration
winston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
// Connect to MongoDB
connectDB();
// Routes
app.use('/orders', orderRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Error Handling Middleware
app.use(errorHandler);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Order Service API');
});
// Start Server
const PORT = process.env.PORT || 5004;
app.listen(PORT, () => {
winston.info(`Order Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3.4.2. controllers/orderController.js
(Already provided above)
3.4.3. models/Order.js
(Already provided above)
3.4.4. routes/orderRoutes.js
(Already provided above)
3.4.5. middleware/authMiddleware.js
(Already provided above)
3.4.6. middleware/errorHandler.js
(Already provided above)
3.4.7. services/rabbitmqService.js
(Already provided above)
3.4.8. config/db.js
(Already provided above)
3.4.9. swagger.js
(Already provided above)
3.4.10. README.md
(Already provided above)
3.5. Testing
Testing ensures that the Order Service behaves as expected. We've already created unit and integration tests above.
3.5.1. Running Tests
1. Run Unit Tests:
bash
Copy code
cd order-service
npm test
2. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
3.6. Linting and Formatting
Maintaining code quality is crucial, especially as the project scales.
1. Install ESLint and Prettier:
(Already installed in dependencies)
2. Initialize ESLint:
bash
Copy code
npx eslint --init
o Choose the following options:
How would you like to use ESLint? To check syntax, find problems, and enforce code style
What type of modules does your project use? CommonJS
Which framework does your project use? None of these
Does your project use TypeScript? No
Where does your code run? Node
How would you like to define a style for your project? Use a popular style guide
Which style guide do you want to follow? Airbnb
What format do you want your config file to be in? JavaScript
Would you like to install them now with npm? Yes
3. Configure Prettier:
Create a .prettierrc file in order-service/:
json
Copy code
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}
4. Add Lint and Format Scripts:
json
Copy code
// order-service/package.json
{
"scripts": {
// ...
"lint": "eslint . --ext .js",
"format": "prettier --write ."
},
// ...
}
5. Run Linting and Formatting:
bash
Copy code
npm run lint
npm run format
3.7. Dockerization
Containerizing the Order Service ensures consistent environments across development, testing, and production.
1. Create Dockerfile:
dockerfile
Copy code
# order-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 5004
# Start the service
CMD [ "npm", "start" ]
2. Update Root docker-compose.yml:
Ensure that the Order Service is included in the Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
# ... other services
volumes:
mongo-data:
Explanation:
o order-service: Configured to build from the order-service directory, expose port 5004, and link to MongoDB and RabbitMQ services.
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Order Service will be accessible at http://localhost:5004/.
o Swagger UI for Order Service: http://localhost:5004/api-docs.
3.8. Final Integration and Deployment
1. Ensure All Services Are Running:
Verify that MongoDB, RabbitMQ, and Order Service are up and running.
2. Update API Gateway Configuration:
Ensure that the API Gateway routes are updated to include the Order Service endpoints.
Example:
yaml
Copy code
# api-gateway/config/gateway.config.yml
serviceEndpoints:
# ... existing services
order-service:
url: 'http://order-service:5004'
pipelines:
ordersPipeline:
apiEndpoints:
- orders
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: order-service
apiEndpoints:
# ... existing endpoints
orders:
path: /orders
target: http://order-service:5004
methods: GET, POST, PUT
authentication:
- jwt-auth
Note: Adjust gateway.config.yml based on your existing configuration structure.
3. Test API Endpoints via API Gateway:
o Create Order: POST http://localhost:8080/orders
o Get All Orders: GET http://localhost:8080/orders
o Get Order by ID: GET http://localhost:8080/orders/:id
o Update Order Status: PUT http://localhost:8080/orders/:id/status
Ensure that JWT tokens are correctly passed in the Authorization header.
4. Deploying to Production:
o Dockerization: Ensure that the Dockerfile and docker-compose.yml are correctly configured for production environments.
o Environment Variables: Securely manage environment variables using services like AWS Secrets Manager or Docker secrets.
o Scaling Services: Utilize Docker Compose scaling or orchestrate with Kubernetes for larger deployments.
________________________________________
4. Complete Code and Configuration Files
4.1. Order Service package.json
json
Copy code
{
"name": "order-service",
"version": "1.0.0",
"description": "Order Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"winston": "^3.8.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js",
"services/**/*.js"
]
},
"config": {
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
4.2. Order Service Directory Structure
arduino
Copy code
order-service/
├── controllers/
│ └── orderController.js
├── models/
│ └── Order.js
├── routes/
│ └── orderRoutes.js
├── services/
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ ├── order.test.js
│ └── orderIntegration.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── jsdoc.json
├── package.json
├── README.md
├── index.js
└── jest.setup.js
4.3. Sample Files
4.3.1. controllers/orderController.js
(Already provided above)
4.3.2. models/Order.js
(Already provided above)
4.3.3. routes/orderRoutes.js
(Already provided above)
4.3.4. middleware/authMiddleware.js
(Already provided above)
4.3.5. middleware/errorHandler.js
(Already provided above)
4.3.6. services/rabbitmqService.js
(Already provided above)
4.3.7. config/db.js
(Already provided above)
4.3.8. swagger.js
(Already provided above)
4.3.9. README.md
(Already provided above)
4.3.10. jest.config.js
(Already provided above)
4.3.11. jest.setup.js
(Already provided above)
4.3.12. jsdoc.json
(Already provided above)
4.3.13. index.js
(Already provided above)
4.4. Dockerization
(Already provided above)
________________________________________
5. Documentation and Usage
5.1. Running the Order Service Locally
1. Ensure MongoDB and RabbitMQ Are Running:
If you're using Docker Compose for all services, ensure that MongoDB and RabbitMQ are up.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d mongodb rabbitmq
2. Start the Order Service:
bash
Copy code
cd order-service
npm run dev
o The service will run on http://localhost:5004/.
o Swagger UI for Order Service: http://localhost:5004/api-docs.
5.2. API Endpoints
Access the Swagger UI to explore and test the API endpoints.
• Create Order: POST /orders
• Get All Orders: GET /orders
• Get Order by ID: GET /orders/{id}
• Update Order Status: PUT /orders/{id}/status
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
5.3. Testing
1. Run Tests:
bash
Copy code
cd order-service
npm test
2. View Coverage:
o Open the coverage/lcov-report/index.html file in a browser to view detailed coverage reports.
5.4. Linting and Formatting
1. Run Linting:
bash
Copy code
npm run lint
2. Run Formatting:
bash
Copy code
npm run format
5.5. Generating Documentation
1. Annotate Code with JSDoc Comments:
Ensure that all controllers, models, and routes are properly documented with JSDoc.
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
5.6. Contributing
1. Fork the Repository:
Create a personal fork of the Order Service repository.
2. Create a Feature Branch:
bash
Copy code
git checkout -b feature/add-order-endpoint
3. Make Changes and Commit:
bash
Copy code
git commit -m "feat(order-service): add create order endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-order-endpoint
o Submit a pull request from your fork to the main repository for review.
________________________________________
6. Troubleshooting Tips
1. Connection Issues with MongoDB:
o Problem: Order Service fails to connect to MongoDB.
o Solution:
Ensure MongoDB is running and accessible at the URI specified in .env.
Check Docker Compose logs for any MongoDB-related errors.
Verify network configurations if MongoDB is hosted externally.
2. RabbitMQ Connection Errors:
o Problem: Order Service cannot connect to RabbitMQ.
o Solution:
Ensure RabbitMQ is running and accessible at the URI specified in .env.
Check RabbitMQ management console (http://localhost:15672) for any connection issues.
Verify credentials and network configurations.
3. JWT Authentication Failures:
o Problem: Receiving 401 Unauthorized errors when accessing protected endpoints.
o Solution:
Ensure that a valid JWT token is being sent in the Authorization header.
Verify that the JWT_SECRET in Order Service matches the one used by Auth Service.
Check token expiration and validity.
4. API Gateway Routing Issues:
o Problem: API calls to /orders are not reaching the Order Service.
o Solution:
Verify that the API Gateway configuration (gateway.config.yml) includes routes to the Order Service.
Ensure that the Order Service is running and accessible to the API Gateway.
Check Docker networking settings to confirm connectivity.
5. Event Publishing Failures:
o Problem: Events are not being published to RabbitMQ upon order creation.
o Solution:
Ensure that RabbitMQ is running and accessible.
Check the rabbitmqService.js for any connection or publishing errors.
Review logs for any RabbitMQ-related error messages.
6. Unhandled Exceptions:
o Problem: The Order Service crashes due to unhandled exceptions.
o Solution:
Review logs to identify the source of the error.
Implement proper error handling in controllers and middleware.
Use try-catch blocks around asynchronous operations.
________________________________________
7. Additional Resources
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• Mongoose Documentation: https://mongoosejs.com/docs/guide.html
• RabbitMQ Tutorials: https://www.rabbitmq.com/getstarted.html
• JWT Documentation: https://jwt.io/introduction/
• Winston Documentation: https://github.com/winstonjs/winston
• Swagger Documentation: https://swagger.io/docs/specification/about/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for the Order Service within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing more advanced logging mechanisms, integrating with payment gateways, or enhancing security measures.
________________________________________
By following this comprehensive guide for Stage 10: Backend Development, you've successfully implemented the Order Service, a critical component of your eCommerce platform. This service not only manages customer orders but also integrates seamlessly with other backend services and facilitates event-driven communication through RabbitMQ.
With a modular and maintainable codebase, robust testing, and thorough documentation, your backend infrastructure is well-equipped to support the growing needs of iBranDIDit.com.
Next Steps:
Proceed to Stage 11: Security Enhancements and Best Practices to further secure your backend services, ensuring data integrity and protection against common vulnerabilities.
Stage 11: Payment & Payouts Integration is a crucial step in enhancing the functionality and reliability of your eCommerce platform, iBranDIDit.com. This stage focuses on integrating payment processing capabilities, enabling seamless transactions for customers and handling payouts for commissions and vendor payments. We'll cover integrating PayPal, implementing a payout API, developing alternative payment gateways, and setting up transaction logging and security protocols.
Each section provides detailed instructions, complete with code snippets, dependencies, configuration files, and documentation to ensure easy integration, even for beginners. This stage builds upon the previous stages, ensuring that dependencies and interactions are correctly handled. By the end of this stage, you'll have a complete, deployable solution where the developer only needs to upload images, content, and perform minor configuration adjustments.
________________________________________
Stage 11: Payment & Payouts Integration
________________________________________
1. Overview
Objective:
• Integrate PayPal for Payment Processing: Enable customers to make payments using PayPal.
• Implement Payout API for Commissions and Vendor Payments: Facilitate automatic payouts to vendors and handle commission distributions.
• Develop Alternative Payment Gateway Options: Provide additional payment methods (e.g., Stripe) to offer customers flexibility.
• Set Up Transaction Logging and Security Protocols: Ensure all transactions are securely logged and protected against vulnerabilities.
Benefits:
• Enhanced Customer Experience: Offering multiple payment options increases customer satisfaction and trust.
• Automated Financial Operations: Streamlining payouts reduces manual workload and minimizes errors.
• Scalability and Flexibility: Integrating multiple payment gateways allows the platform to adapt to diverse customer preferences.
• Robust Security and Compliance: Ensuring secure transaction processing protects sensitive data and maintains regulatory compliance.
________________________________________
2. Technology Stack
• Runtime Environment: Node.js v14 or higher
• Framework: Express.js – For building the RESTful API
• Database: MongoDB – For storing transaction data
• ODM: Mongoose – For MongoDB object modeling
• Payment Gateways:
o PayPal: PayPal REST SDK or PayPal Checkout SDK
o Stripe: Stripe Node.js SDK
• Authentication: JWT – For securing API endpoints
• Logging: Winston – For logging application events
• Configuration Management: dotenv – For managing environment variables
• Message Broker: RabbitMQ – For event-driven communication
• Testing: Jest and Supertest – For unit and integration testing
• Version Control: Git – Continuation from Stage 8
• Documentation: Markdown and Swagger – For API documentation
________________________________________
3. Implementation Steps
3.1. Setting Up the Payment Service Directory
We'll create a new microservice called Payment Service responsible for handling all payment-related operations.
1. Navigate to the Root Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Create the Payment Service Workspace:
bash
Copy code
mkdir payment-service
3. Initialize a New Node.js Project:
bash
Copy code
cd payment-service
npm init -y
4. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv jsonwebtoken amqplib winston paypal-rest-sdk stripe
5. Install Development Dependencies:
bash
Copy code
npm install --save-dev nodemon jest supertest eslint prettier swagger-jsdoc swagger-ui-express
6. Update Root package.json:
Add payment-service to the workspaces array.
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service",
"frontend",
"order-service",
"payment-service"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\"",
"frontend": "npm --workspace frontend start",
"order-service": "npm --workspace order-service start",
"payment-service": "npm --workspace payment-service start"
},
"dependencies": {},
"devDependencies": {}
}
3.2. Project Structure Overview
The payment-service/ directory will have the following structure:
lua
Copy code
payment-service/
├── controllers/
│ ├── paymentController.js
│ └── payoutController.js
├── models/
│ ├── Transaction.js
│ └── Payout.js
├── routes/
│ ├── paymentRoutes.js
│ └── payoutRoutes.js
├── services/
│ ├── paypalService.js
│ ├── stripeService.js
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ ├── payment.test.js
│ └── payout.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
├── swagger.js
└── index.js
Explanation:
• controllers/: Contains business logic for handling payment and payout requests.
• models/: Defines Mongoose schemas for transactions and payouts.
• routes/: Defines API endpoints and associates them with controllers.
• services/: Handles external services like PayPal, Stripe, and RabbitMQ.
• middleware/: Contains middleware for authentication and error handling.
• config/: Manages configuration, such as database connections.
• tests/: Contains unit and integration tests.
• .env.example: Template for environment variables.
• .gitignore: Specifies files and directories to ignore in Git.
• jest.config.js: Configuration for Jest testing framework.
• README.md: Documentation for the Payment Service.
• swagger.js: Configures Swagger for API documentation.
• index.js: Entry point of the application.
3.3. Developing the Payment Service Components
3.3.1. Configuration Files
1. .gitignore
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# OS files
.DS_Store
Thumbs.db
2. .env.example
env
Copy code
PORT=5005
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# PayPal Configuration
PAYPAL_MODE=sandbox
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
# Stripe Configuration
STRIPE_SECRET_KEY=your_stripe_secret_key
Descriptions:
o PORT: Port on which the Payment Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
o PAYPAL_*: PayPal API credentials.
o STRIPE_SECRET_KEY: Stripe API secret key.
3. jest.config.js
javascript
Copy code
// payment-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js',
'services/**/*.js'
]
};
4. jest.setup.js
(Create this file in the root of payment-service/)
javascript
Copy code
// payment-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
5. jsdoc.json
json
Copy code
// payment-service/jsdoc.json
{
"source": {
"include": ["./routes", "./controllers", "./models", "./middleware"],
"exclude": ["node_modules"],
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
}
}
}
3.3.2. Database Configuration
1. config/db.js
javascript
Copy code
// payment-service/config/db.js
const mongoose = require('mongoose');
const winston = require('winston');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
winston.info('Payment Service connected to MongoDB');
} catch (error) {
winston.error('Payment Service failed to connect to MongoDB:', error);
process.exit(1);
}
};
module.exports = connectDB;
3.3.3. Models
1. models/Transaction.js
javascript
Copy code
// payment-service/models/Transaction.js
const mongoose = require('mongoose');
const TransactionSchema = new mongoose.Schema({
orderId: {
type: String,
required: true,
ref: 'Order', // Reference to Order model in Order Service
},
userId: {
type: String,
required: true,
ref: 'User', // Reference to User model in User Service
},
amount: {
type: Number,
required: true,
min: 0,
},
paymentMethod: {
type: String,
enum: ['PayPal', 'Stripe'],
required: true,
},
paymentStatus: {
type: String,
enum: ['Pending', 'Completed', 'Failed'],
default: 'Pending',
},
transactionId: {
type: String,
required: false,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
TransactionSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Transaction', TransactionSchema);
2. models/Payout.js
javascript
Copy code
// payment-service/models/Payout.js
const mongoose = require('mongoose');
const PayoutSchema = new mongoose.Schema({
vendorId: {
type: String,
required: true,
ref: 'Vendor', // Reference to Vendor model in Vendor Service
},
amount: {
type: Number,
required: true,
min: 0,
},
payoutMethod: {
type: String,
enum: ['BankTransfer', 'PayPal'],
required: true,
},
payoutStatus: {
type: String,
enum: ['Pending', 'Completed', 'Failed'],
default: 'Pending',
},
transactionId: {
type: String,
required: false,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
PayoutSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Payout', PayoutSchema);
3.3.4. Controllers
1. controllers/paymentController.js
javascript
Copy code
// payment-service/controllers/paymentController.js
const Transaction = require('../models/Transaction');
const paypalService = require('../services/paypalService');
const stripeService = require('../services/stripeService');
const rabbitmqService = require('../services/rabbitmqService');
const winston = require('winston');
/**
* Create a new payment
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const createPayment = async (req, res) => {
try {
const { orderId, userId, amount, paymentMethod } = req.body;
if (!orderId || !userId || !amount || !paymentMethod) {
return res.status(400).json({ message: 'Invalid payment data' });
}
if (!['PayPal', 'Stripe'].includes(paymentMethod)) {
return res.status(400).json({ message: 'Unsupported payment method' });
}
// Create a new transaction with status 'Pending'
const transaction = new Transaction({
orderId,
userId,
amount,
paymentMethod,
paymentStatus: 'Pending',
});
const savedTransaction = await transaction.save();
let paymentResponse;
if (paymentMethod === 'PayPal') {
paymentResponse = await paypalService.createPayment(savedTransaction);
} else if (paymentMethod === 'Stripe') {
paymentResponse = await stripeService.createPayment(savedTransaction);
}
res.status(201).json({
message: 'Payment initiated',
paymentData: paymentResponse,
});
} catch (error) {
winston.error('Error creating payment:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Handle PayPal payment execution callback
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const executePayPalPayment = async (req, res) => {
try {
const { paymentId, payerId } = req.query;
const { transactionId } = req.body;
if (!paymentId || !payerId || !transactionId) {
return res.status(400).json({ message: 'Missing required parameters' });
}
const transaction = await Transaction.findById(transactionId);
if (!transaction) {
return res.status(404).json({ message: 'Transaction not found' });
}
const executeResult = await paypalService.executePayment(paymentId, payerId);
if (executeResult.state === 'approved') {
transaction.paymentStatus = 'Completed';
transaction.transactionId = executeResult.id;
await transaction.save();
// Publish payment completed event
await rabbitmqService.publishEvent('payment_completed', {
transactionId: transaction._id,
orderId: transaction.orderId,
userId: transaction.userId,
amount: transaction.amount,
paymentMethod: transaction.paymentMethod,
});
res.status(200).json({ message: 'Payment completed successfully' });
} else {
transaction.paymentStatus = 'Failed';
await transaction.save();
res.status(400).json({ message: 'Payment not approved' });
}
} catch (error) {
winston.error('Error executing PayPal payment:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Handle Stripe webhook for payment confirmation
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const handleStripeWebhook = async (req, res) => {
try {
const event = req.body;
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
const transaction = await Transaction.findOne({ transactionId: paymentIntent.id });
if (transaction) {
transaction.paymentStatus = 'Completed';
await transaction.save();
// Publish payment completed event
await rabbitmqService.publishEvent('payment_completed', {
transactionId: transaction._id,
orderId: transaction.orderId,
userId: transaction.userId,
amount: transaction.amount,
paymentMethod: transaction.paymentMethod,
});
}
break;
case 'payment_intent.payment_failed':
const failedIntent = event.data.object;
const failedTransaction = await Transaction.findOne({ transactionId: failedIntent.id });
if (failedTransaction) {
failedTransaction.paymentStatus = 'Failed';
await failedTransaction.save();
}
break;
// ... handle other event types
default:
winston.warn(`Unhandled event type ${event.type}`);
}
res.json({ received: true });
} catch (error) {
winston.error('Error handling Stripe webhook:', error);
res.status(500).json({ message: 'Server error' });
}
};
module.exports = {
createPayment,
executePayPalPayment,
handleStripeWebhook,
};
2. controllers/payoutController.js
javascript
Copy code
// payment-service/controllers/payoutController.js
const Payout = require('../models/Payout');
const paypalService = require('../services/paypalService');
const stripeService = require('../services/stripeService');
const rabbitmqService = require('../services/rabbitmqService');
const winston = require('winston');
/**
* Create a new payout
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const createPayout = async (req, res) => {
try {
const { vendorId, amount, payoutMethod } = req.body;
if (!vendorId || !amount || !payoutMethod) {
return res.status(400).json({ message: 'Invalid payout data' });
}
if (!['BankTransfer', 'PayPal'].includes(payoutMethod)) {
return res.status(400).json({ message: 'Unsupported payout method' });
}
// Create a new payout with status 'Pending'
const payout = new Payout({
vendorId,
amount,
payoutMethod,
payoutStatus: 'Pending',
});
const savedPayout = await payout.save();
let payoutResponse;
if (payoutMethod === 'PayPal') {
payoutResponse = await paypalService.createPayout(savedPayout);
} else if (payoutMethod === 'BankTransfer') {
payoutResponse = await stripeService.createPayout(savedPayout);
}
res.status(201).json({
message: 'Payout initiated',
payoutData: payoutResponse,
});
} catch (error) {
winston.error('Error creating payout:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Handle PayPal payout callback
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const executePayPalPayout = async (req, res) => {
try {
const { payoutId, transactionId } = req.body;
if (!payoutId || !transactionId) {
return res.status(400).json({ message: 'Missing required parameters' });
}
const payout = await Payout.findById(transactionId);
if (!payout) {
return res.status(404).json({ message: 'Payout not found' });
}
const executeResult = await paypalService.executePayout(payoutId);
if (executeResult.batch_status === 'SUCCESS') {
payout.payoutStatus = 'Completed';
payout.transactionId = executeResult.batch_header.payout_batch_id;
await payout.save();
// Publish payout completed event
await rabbitmqService.publishEvent('payout_completed', {
payoutId: payout._id,
vendorId: payout.vendorId,
amount: payout.amount,
payoutMethod: payout.payoutMethod,
});
res.status(200).json({ message: 'Payout completed successfully' });
} else {
payout.payoutStatus = 'Failed';
await payout.save();
res.status(400).json({ message: 'Payout not approved' });
}
} catch (error) {
winston.error('Error executing PayPal payout:', error);
res.status(500).json({ message: 'Server error' });
}
};
module.exports = {
createPayout,
executePayPalPayout,
};
3.3.5. Routes
1. routes/paymentRoutes.js
javascript
Copy code
// payment-service/routes/paymentRoutes.js
const express = require('express');
const router = express.Router();
const paymentController = require('../controllers/paymentController');
const authMiddleware = require('../middleware/authMiddleware');
/**
* @swagger
* tags:
* name: Payments
* description: Payment processing
*/
/**
* @swagger
* /payments:
* post:
* summary: Initiate a new payment
* tags: [Payments]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* orderId:
* type: string
* userId:
* type: string
* amount:
* type: number
* paymentMethod:
* type: string
* enum: ['PayPal', 'Stripe']
* required:
* - orderId
* - userId
* - amount
* - paymentMethod
* example:
* orderId: '60d5f9f9b1e8e1d4c8e4f8c5'
* userId: '60d5f9f9b1e8e1d4c8e4f8c5'
* amount: 99.99
* paymentMethod: 'PayPal'
* responses:
* 201:
* description: Payment initiated successfully
* 400:
* description: Invalid payment data
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.post('/', authMiddleware, paymentController.createPayment);
/**
* @swagger
* /payments/paypal/execute:
* post:
* summary: Execute PayPal payment after approval
* tags: [Payments]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* paymentId:
* type: string
* payerId:
* type: string
* transactionId:
* type: string
* required:
* - paymentId
* - payerId
* - transactionId
* example:
* paymentId: 'PAYID-LG8P...'
* payerId: 'Q7V76...'
* transactionId: '60d5f9f9b1e8e1d4c8e4f8c5'
* responses:
* 200:
* description: Payment executed successfully
* 400:
* description: Payment not approved or invalid parameters
* 401:
* description: Unauthorized
* 404:
* description: Transaction not found
* 500:
* description: Server error
*/
router.post('/paypal/execute', authMiddleware, paymentController.executePayPalPayment);
/**
* @swagger
* /payments/stripe/webhook:
* post:
* summary: Handle Stripe webhook events
* tags: [Payments]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* description: Stripe event object
* responses:
* 200:
* description: Webhook received successfully
* 400:
* description: Invalid payload
* 500:
* description: Server error
*/
router.post('/stripe/webhook', paymentController.handleStripeWebhook);
module.exports = router;
2. routes/payoutRoutes.js
javascript
Copy code
// payment-service/routes/payoutRoutes.js
const express = require('express');
const router = express.Router();
const payoutController = require('../controllers/payoutController');
const authMiddleware = require('../middleware/authMiddleware');
/**
* @swagger
* tags:
* name: Payouts
* description: Vendor payouts
*/
/**
* @swagger
* /payouts:
* post:
* summary: Initiate a new payout
* tags: [Payouts]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* vendorId:
* type: string
* amount:
* type: number
* payoutMethod:
* type: string
* enum: ['BankTransfer', 'PayPal']
* required:
* - vendorId
* - amount
* - payoutMethod
* example:
* vendorId: '60d5f9f9b1e8e1d4c8e4f8c5'
* amount: 150.00
* payoutMethod: 'PayPal'
* responses:
* 201:
* description: Payout initiated successfully
* 400:
* description: Invalid payout data
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.post('/', authMiddleware, payoutController.createPayout);
/**
* @swagger
* /payouts/paypal/execute:
* post:
* summary: Execute PayPal payout after approval
* tags: [Payouts]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* payoutId:
* type: string
* transactionId:
* type: string
* required:
* - payoutId
* - transactionId
* example:
* payoutId: 'PAYOUTID-LG8P...'
* transactionId: '60d5f9f9b1e8e1d4c8e4f8c5'
* responses:
* 200:
* description: Payout executed successfully
* 400:
* description: Payout not approved or invalid parameters
* 401:
* description: Unauthorized
* 404:
* description: Payout not found
* 500:
* description: Server error
*/
router.post('/paypal/execute', authMiddleware, payoutController.executePayPalPayout);
module.exports = router;
3.3.6. Middleware
1. middleware/authMiddleware.js
javascript
Copy code
// payment-service/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const winston = require('winston');
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authorization token missing or malformed' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
winston.error('Invalid token:', error);
return res.status(401).json({ message: 'Invalid or expired token' });
}
};
module.exports = authMiddleware;
2. middleware/errorHandler.js
javascript
Copy code
// payment-service/middleware/errorHandler.js
const winston = require('winston');
const errorHandler = (err, req, res, next) => {
winston.error(err.message, err);
res.status(500).json({ message: 'Server error' });
};
module.exports = errorHandler;
3.3.7. Services
1. services/paypalService.js
javascript
Copy code
// payment-service/services/paypalService.js
const paypal = require('paypal-rest-sdk');
const winston = require('winston');
// Configure PayPal SDK
paypal.configure({
mode: process.env.PAYPAL_MODE, // 'sandbox' or 'live'
client_id: process.env.PAYPAL_CLIENT_ID,
client_secret: process.env.PAYPAL_CLIENT_SECRET,
});
/**
* Create a PayPal payment
* @param {Object} transaction - Transaction details
* @returns {Promise<Object>} PayPal payment object
*/
const createPayment = (transaction) => {
return new Promise((resolve, reject) => {
const payment = {
intent: 'sale',
payer: {
payment_method: 'paypal',
},
redirect_urls: {
return_url: 'http://localhost:5005/payments/paypal/execute',
cancel_url: 'http://localhost:5005/payments/cancel',
},
transactions: [
{
item_list: {
items: [
{
name: `Order ${transaction.orderId}`,
sku: 'item',
price: transaction.amount.toFixed(2),
currency: 'USD',
quantity: 1,
},
],
},
amount: {
currency: 'USD',
total: transaction.amount.toFixed(2),
},
description: `Payment for Order ID: ${transaction.orderId}`,
},
],
};
paypal.payment.create(payment, (error, payment) => {
if (error) {
winston.error('PayPal payment creation failed:', error.response);
reject(error);
} else {
resolve(payment);
}
});
});
};
/**
* Execute a PayPal payment
* @param {string} paymentId - PayPal payment ID
* @param {string} payerId - PayPal payer ID
* @returns {Promise<Object>} Executed payment object
*/
const executePayment = (paymentId, payerId) => {
return new Promise((resolve, reject) => {
const execution = {
payer_id: payerId,
};
paypal.payment.execute(paymentId, execution, (error, payment) => {
if (error) {
winston.error('PayPal payment execution failed:', error.response);
reject(error);
} else {
resolve(payment);
}
});
});
};
/**
* Create a PayPal payout
* @param {Object} payout - Payout details
* @returns {Promise<Object>} PayPal payout object
*/
const createPayout = (payout) => {
return new Promise((resolve, reject) => {
const create_payout_json = {
sender_batch_header: {
sender_batch_id: `batch_${payout._id}`,
email_subject: 'You have a payout from iBranDIDit.com',
},
items: [
{
recipient_type: 'EMAIL',
amount: {
value: payout.amount.toFixed(2),
currency: 'USD',
},
receiver: 'vendor@example.com', // Replace with vendor's PayPal email
note: `Payout for vendor ID: ${payout.vendorId}`,
sender_item_id: `item_${payout._id}`,
},
],
};
paypal.payout.create(create_payout_json, (error, payout) => {
if (error) {
winston.error('PayPal payout creation failed:', error.response);
reject(error);
} else {
resolve(payout);
}
});
});
};
/**
* Execute a PayPal payout
* @param {string} payoutId - PayPal payout ID
* @returns {Promise<Object>} Executed payout object
*/
const executePayout = (payoutId) => {
return new Promise((resolve, reject) => {
paypal.payout.get(payoutId, (error, payout) => {
if (error) {
winston.error('PayPal payout execution failed:', error.response);
reject(error);
} else {
resolve(payout);
}
});
});
};
module.exports = {
createPayment,
executePayment,
createPayout,
executePayout,
};
2. services/stripeService.js
javascript
Copy code
// payment-service/services/stripeService.js
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
const winston = require('winston');
/**
* Create a Stripe payment intent
* @param {Object} transaction - Transaction details
* @returns {Promise<Object>} Stripe payment intent
*/
const createPayment = async (transaction) => {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(transaction.amount * 100), // Convert to cents
currency: 'usd',
metadata: { orderId: transaction.orderId, transactionId: transaction._id.toString() },
});
return paymentIntent;
} catch (error) {
winston.error('Stripe payment creation failed:', error);
throw error;
}
};
/**
* Create a Stripe payout
* @param {Object} payout - Payout details
* @returns {Promise<Object>} Stripe payout
*/
const createPayout = async (payout) => {
try {
const payoutObject = await stripe.payouts.create({
amount: Math.round(payout.amount * 100), // Convert to cents
currency: 'usd',
});
return payoutObject;
} catch (error) {
winston.error('Stripe payout creation failed:', error);
throw error;
}
};
module.exports = {
createPayment,
createPayout,
};
3. services/rabbitmqService.js
javascript
Copy code
// payment-service/services/rabbitmqService.js
const amqp = require('amqplib');
const winston = require('winston');
let channel = null;
const connectRabbitMQ = async () => {
try {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
winston.info('Payment Service connected to RabbitMQ');
} catch (error) {
winston.error('Payment Service failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const publishEvent = async (event, data) => {
if (!channel) {
await connectRabbitMQ();
}
const queue = 'payment_events';
await channel.assertQueue(queue, { durable: true });
const payload = { event, data };
channel.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
winston.info(`Published event to RabbitMQ: ${event}`);
};
connectRabbitMQ();
module.exports = {
publishEvent,
};
3.3.8. Controllers
(Already provided above in controllers/paymentController.js and controllers/payoutController.js)
3.3.9. Routes
(Already provided above in routes/paymentRoutes.js and routes/payoutRoutes.js)
3.3.10. Entry Point
1. index.js
javascript
Copy code
// payment-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const bodyParser = require('body-parser');
const winston = require('winston');
const connectDB = require('./config/db');
const paymentRoutes = require('./routes/paymentRoutes');
const payoutRoutes = require('./routes/payoutRoutes');
const errorHandler = require('./middleware/errorHandler');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Logging configuration
winston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
// Connect to MongoDB
connectDB();
// Routes
app.use('/payments', paymentRoutes);
app.use('/payouts', payoutRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Error Handling Middleware
app.use(errorHandler);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Payment Service API');
});
// Start Server
const PORT = process.env.PORT || 5005;
app.listen(PORT, () => {
winston.info(`Payment Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3.3.11. Services
(Already provided above in services/paypalService.js, services/stripeService.js, and services/rabbitmqService.js)
3.3.12. Testing
1. Unit Tests
o tests/payment.test.js
javascript
Copy code
// payment-service/tests/payment.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../index');
const Transaction = require('../models/Transaction');
const jwt = require('jsonwebtoken');
// Generate a test JWT token
const testUser = {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(testUser, process.env.JWT_SECRET, { expiresIn: '1h' });
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/payment_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await Transaction.deleteMany({});
await mongoose.connection.close();
});
describe('Payment API Endpoints', () => {
it('should initiate a new PayPal payment', async () => {
const res = await request(app)
.post('/payments')
.set('Authorization', `Bearer ${token}`)
.send({
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
userId: testUser.id,
amount: 99.99,
paymentMethod: 'PayPal',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Payment initiated');
expect(res.body).toHaveProperty('paymentData');
});
it('should not initiate payment with unsupported method', async () => {
const res = await request(app)
.post('/payments')
.set('Authorization', `Bearer ${token}`)
.send({
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
userId: testUser.id,
amount: 99.99,
paymentMethod: 'UnsupportedMethod',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Unsupported payment method');
});
it('should execute a PayPal payment', async () => {
// Mock payment creation and execution would be needed here
// For simplicity, this test can be expanded with proper mocking
// Alternatively, use integration tests to handle real API calls
expect(true).toBe(true);
});
it('should handle Stripe webhook', async () => {
const res = await request(app)
.post('/payments/stripe/webhook')
.send({
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_123456789',
metadata: {
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
transactionId: '60d5f9f9b1e8e1d4c8e4f8c6',
},
},
},
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('received', true);
});
});
o tests/payout.test.js
javascript
Copy code
// payment-service/tests/payout.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../index');
const Payout = require('../models/Payout');
const jwt = require('jsonwebtoken');
// Generate a test JWT token
const testUser = {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(testUser, process.env.JWT_SECRET, { expiresIn: '1h' });
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/payment_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await Payout.deleteMany({});
await mongoose.connection.close();
});
describe('Payout API Endpoints', () => {
it('should initiate a new payout', async () => {
const res = await request(app)
.post('/payouts')
.set('Authorization', `Bearer ${token}`)
.send({
vendorId: '60d5f9f9b1e8e1d4c8e4f8c7',
amount: 150.0,
payoutMethod: 'PayPal',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Payout initiated');
expect(res.body).toHaveProperty('payoutData');
});
it('should not initiate payout with unsupported method', async () => {
const res = await request(app)
.post('/payouts')
.set('Authorization', `Bearer ${token}`)
.send({
vendorId: '60d5f9f9b1e8e1d4c8e4f8c7',
amount: 150.0,
payoutMethod: 'UnsupportedMethod',
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Unsupported payout method');
});
it('should execute a PayPal payout', async () => {
// Mock payout creation and execution would be needed here
// For simplicity, this test can be expanded with proper mocking
// Alternatively, use integration tests to handle real API calls
expect(true).toBe(true);
});
});
3.3.13. Swagger Configuration
1. swagger.js
javascript
Copy code
// payment-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Payment Service API',
version: '1.0.0',
description: 'API documentation for the Payment Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5005',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
PaymentInput: {
type: 'object',
required: ['orderId', 'userId', 'amount', 'paymentMethod'],
properties: {
orderId: {
type: 'string',
description: 'Order ID referencing the Order model',
},
userId: {
type: 'string',
description: 'User ID referencing the User model',
},
amount: {
type: 'number',
description: 'Total amount for the payment',
},
paymentMethod: {
type: 'string',
enum: ['PayPal', 'Stripe'],
description: 'Method of payment',
},
},
example: {
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
userId: '60d5f9f9b1e8e1d4c8e4f8c5',
amount: 99.99,
paymentMethod: 'PayPal',
},
},
PaymentOutput: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Status message',
},
paymentData: {
type: 'object',
description: 'Payment gateway response data',
},
},
},
PayoutInput: {
type: 'object',
required: ['vendorId', 'amount', 'payoutMethod'],
properties: {
vendorId: {
type: 'string',
description: 'Vendor ID referencing the Vendor model',
},
amount: {
type: 'number',
description: 'Amount to payout',
},
payoutMethod: {
type: 'string',
enum: ['BankTransfer', 'PayPal'],
description: 'Method of payout',
},
},
example: {
vendorId: '60d5f9f9b1e8e1d4c8e4f8c7',
amount: 150.0,
payoutMethod: 'PayPal',
},
},
PayoutOutput: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Status message',
},
payoutData: {
type: 'object',
description: 'Payout gateway response data',
},
},
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
2. Annotate Routes with Swagger Comments
(Already provided in routes/paymentRoutes.js and routes/payoutRoutes.js above)
3.3.14. README Documentation
1. README.md
markdown
Copy code
# Payment Service
## Overview
The **Payment Service** is a microservice responsible for handling all payment-related operations in the iBranDIDit.com eCommerce platform. It manages payment processing through multiple gateways (PayPal and Stripe), handles vendor payouts, and ensures secure and reliable financial transactions.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Logging](#logging)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [MongoDB](https://www.mongodb.com/) (local or hosted)
- [RabbitMQ](https://www.rabbitmq.com/) (local or hosted)
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- [PayPal Developer Account](https://developer.paypal.com/) for API credentials
- [Stripe Developer Account](https://stripe.com/docs/development) for API credentials
## Installation
1. **Navigate to the Payment Service Directory:**
```bash
cd payment-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the payment-service/ directory based on the provided .env.example.
env
Copy code
PORT=5005
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# PayPal Configuration
PAYPAL_MODE=sandbox
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
# Stripe Configuration
STRIPE_SECRET_KEY=your_stripe_secret_key
Descriptions:
o PORT: Port on which the Payment Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
o PAYPAL_*: PayPal API credentials.
o STRIPE_SECRET_KEY: Stripe API secret key.
Note: Ensure that sensitive information like API keys and secrets are kept secure and never committed to version control.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
3. Start the Service:
bash
Copy code
docker-compose up --build payment-service
Note: Ensure that MongoDB and RabbitMQ services are also running, either via the root docker-compose.yml or individually.
Option 2: Running Locally
4. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
5. Start the Payment Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
o URL: http://localhost:5005/api-docs
Payment Endpoints
o Initiate Payment: POST /payments
o Execute PayPal Payment: POST /payments/paypal/execute
o Handle Stripe Webhook: POST /payments/stripe/webhook
Payout Endpoints
o Initiate Payout: POST /payouts
o Execute PayPal Payout: POST /payouts/paypal/execute
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
Testing
Testing ensures that the Payment Service behaves as expected.
6. Run Tests:
bash
Copy code
cd payment-service
npm test
7. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
Logging
The Payment Service uses Winston for logging. Logs are output to the console in a simple format. You can configure additional transports (e.g., file, external logging services) by modifying the Winston configuration in index.js.
Contribution
Contributions are welcome! Please adhere to the following guidelines:
8. Fork the Repository:
Create a personal fork of the Payment Service repository.
9. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/add-payment-endpoint
10. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(payment-service): add payment initiation endpoint"
11. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-payment-endpoint
Submit a pull request from your fork to the main repository for review.
License
MIT
________________________________________
Copy code
3.3.14. Dockerization
1. Create Dockerfile
dockerfile
Copy code
# payment-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 5005
# Start the service
CMD [ "npm", "start" ]
2. Update Root docker-compose.yml
Ensure that the Payment Service is included in the Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
# ... other services
volumes:
mongo-data:
Explanation:
o payment-service: Configured to build from the payment-service directory, expose port 5005, and link to MongoDB and RabbitMQ services.
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Payment Service will be accessible at http://localhost:5005/.
o Swagger UI for Payment Service: http://localhost:5005/api-docs.
________________________________________
4. Complete Code and Configuration Files
4.1. Payment Service package.json
json
Copy code
{
"name": "payment-service",
"version": "1.0.0",
"description": "Payment Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"paypal-rest-sdk": "^2.0.1",
"stripe": "^12.11.0",
"winston": "^3.8.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js",
"services/**/*.js"
]
},
"config": {
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
4.2. Payment Service Directory Structure
lua
Copy code
payment-service/
├── controllers/
│ ├── paymentController.js
│ └── payoutController.js
├── models/
│ ├── Transaction.js
│ └── Payout.js
├── routes/
│ ├── paymentRoutes.js
│ └── payoutRoutes.js
├── services/
│ ├── paypalService.js
│ ├── stripeService.js
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ ├── payment.test.js
│ └── payout.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── jsdoc.json
├── package.json
├── README.md
├── swagger.js
└── index.js
4.3. Sample Component Files
4.3.1. controllers/paymentController.js
(Already provided above)
4.3.2. controllers/payoutController.js
(Already provided above)
4.3.3. models/Transaction.js
(Already provided above)
4.3.4. models/Payout.js
(Already provided above)
4.3.5. routes/paymentRoutes.js
(Already provided above)
4.3.6. routes/payoutRoutes.js
(Already provided above)
4.3.7. middleware/authMiddleware.js
(Already provided above)
4.3.8. middleware/errorHandler.js
(Already provided above)
4.3.9. services/paypalService.js
(Already provided above)
4.3.10. services/stripeService.js
(Already provided above)
4.3.11. services/rabbitmqService.js
(Already provided above)
4.3.12. config/db.js
(Already provided above)
4.3.13. swagger.js
(Already provided above)
4.3.14. index.js
(Already provided above)
4.3.15. README.md
(Already provided above)
________________________________________
5. Documentation and Usage
5.1. Running the Payment Service Locally
1. Ensure MongoDB and RabbitMQ Are Running:
If you're using Docker Compose for all services, ensure that MongoDB and RabbitMQ are up.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d mongodb rabbitmq
2. Start the Payment Service:
bash
Copy code
cd payment-service
npm run dev
o The service will run on http://localhost:5005/.
o Swagger UI for Payment Service: http://localhost:5005/api-docs.
5.2. API Endpoints
Access the Swagger UI to explore and test the API endpoints.
• Payment Endpoints:
o Initiate Payment: POST /payments
o Execute PayPal Payment: POST /payments/paypal/execute
o Handle Stripe Webhook: POST /payments/stripe/webhook
• Payout Endpoints:
o Initiate Payout: POST /payouts
o Execute PayPal Payout: POST /payouts/paypal/execute
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
5.3. Testing
Testing ensures that the Payment Service behaves as expected.
1. Run Tests:
bash
Copy code
cd payment-service
npm test
2. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
5.4. Linting and Formatting
1. Run Linting:
bash
Copy code
npm run lint
2. Run Formatting:
bash
Copy code
npm run format
5.5. Generating Documentation
1. Annotate Code with JSDoc Comments:
Ensure that all controllers, models, and routes are properly documented with JSDoc.
javascript
Copy code
// payment-service/controllers/paymentController.js
/**
* Create a new payment
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Object} JSON response with payment initiation status
*/
const createPayment = async (req, res) => { /* ... */ };
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
5.6. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the Payment Service repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/add-payout-endpoint
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(payment-service): add payout initiation endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-payout-endpoint
o Submit a pull request from your fork to the main repository for review.
________________________________________
6. Integration with Previous Services
6.1. Updating API Gateway Configuration
Ensure that the API Gateway routes are updated to include the Payment Service and Payout Service endpoints.
1. Edit gateway.config.yml:
yaml
Copy code
# api-gateway/config/gateway.config.yml
serviceEndpoints:
# ... existing services
payment-service:
url: 'http://payment-service:5005'
payout-service:
url: 'http://payment-service:5005' # Assuming payout routes are within Payment Service
pipelines:
# ... existing pipelines
paymentsPipeline:
apiEndpoints:
- payments
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: payment-service
payoutsPipeline:
apiEndpoints:
- payouts
policies:
- jwt-auth:
- action:
secretOrPublicKey: 'your_jwt_secret_key' # Replace with actual JWT_SECRET
algorithms:
- HS256
- proxy:
- action:
serviceEndpoint: payout-service
apiEndpoints:
# ... existing endpoints
payments:
path: /payments
target: http://payment-service:5005
methods: GET, POST, PUT
authentication:
- jwt-auth
payouts:
path: /payouts
target: http://payment-service:5005
methods: GET, POST, PUT
authentication:
- jwt-auth
Note: Adjust gateway.config.yml based on your existing configuration structure and ensure that the path and serviceEndpoint correctly map to the Payment Service.
2. Restart API Gateway:
bash
Copy code
docker-compose restart api-gateway
6.2. Testing API Integration
1. Initiate a Payment:
bash
Copy code
curl -X POST http://localhost:8080/payments \
-H "Authorization: Bearer your_jwt_token" \
-H "Content-Type: application/json" \
-d '{
"orderId": "60d5f9f9b1e8e1d4c8e4f8c5",
"userId": "60d5f9f9b1e8e1d4c8e4f8c5",
"amount": 99.99,
"paymentMethod": "PayPal"
}'
2. Execute a PayPal Payment:
Follow the PayPal payment flow as documented in the Swagger UI or via your frontend application.
3. Initiate a Payout:
bash
Copy code
curl -X POST http://localhost:8080/payouts \
-H "Authorization: Bearer your_jwt_token" \
-H "Content-Type: application/json" \
-d '{
"vendorId": "60d5f9f9b1e8e1d4c8e4f8c7",
"amount": 150.00,
"payoutMethod": "PayPal"
}'
________________________________________
7. Additional Tools and Best Practices
1. Secure Environment Variables:
o Never Commit .env Files: Ensure that .env files are excluded via .gitignore.
o Use Environment Variable Management: For production, utilize services like AWS Secrets Manager, Docker secrets, or environment variable injection provided by hosting platforms.
2. Implement HTTPS:
o Use SSL/TLS Certificates: Secure API endpoints by enforcing HTTPS, especially for handling sensitive payment data.
o Configure API Gateway for HTTPS: Ensure that the API Gateway is set up to handle secure connections.
3. Validation and Sanitization:
o Input Validation: Use libraries like Joi to validate incoming request data.
o Sanitization: Prevent injection attacks by sanitizing inputs.
4. Error Handling:
o Consistent Error Responses: Ensure that all errors follow a consistent structure.
o Logging Sensitive Data: Avoid logging sensitive information like credit card details or personal identifiable information (PII).
5. Scalability:
o Horizontal Scaling: Design services to run multiple instances behind a load balancer.
o Stateless Services: Ensure that services are stateless to facilitate scaling.
6. Monitoring and Alerting:
o Use Monitoring Tools: Integrate tools like Prometheus and Grafana for real-time monitoring.
o Set Up Alerts: Configure alerts for critical issues like payment failures or service downtimes.
7. API Rate Limiting:
o Protect Against Abuse: Implement rate limiting to prevent API abuse using middleware like express-rate-limit.
________________________________________
8. Troubleshooting Tips
1. Payment Gateway Integration Issues:
o Problem: Unable to initiate or execute payments with PayPal or Stripe.
o Solution:
Verify API credentials in .env.
Ensure that PayPal and Stripe SDKs are correctly configured.
Check network connectivity to payment gateways.
Review logs for detailed error messages.
2. Webhook Handling Failures:
o Problem: Stripe webhooks are not being received or processed.
o Solution:
Ensure that webhook endpoints are publicly accessible (use tools like ngrok during development).
Verify webhook signing secrets and event types.
Check logs for any errors during webhook processing.
3. RabbitMQ Connectivity Problems:
o Problem: Unable to connect to RabbitMQ for event publishing.
o Solution:
Verify RabbitMQ service is running.
Check connection string and credentials in .env.
Ensure network configurations allow connectivity.
4. MongoDB Connection Errors:
o Problem: Payment Service fails to connect to MongoDB.
o Solution:
Ensure MongoDB is running and accessible at the URI specified in .env.
Check Docker Compose logs for any MongoDB-related errors.
Verify network configurations if MongoDB is hosted externally.
5. Authentication Failures:
o Problem: Receiving 401 Unauthorized errors when accessing protected endpoints.
o Solution:
Ensure that a valid JWT token is being sent in the Authorization header.
Verify that the JWT_SECRET in Payment Service matches the one used by Auth Service.
Check token expiration and validity.
6. CORS Issues:
o Problem: API calls from frontend are blocked due to CORS policy.
o Solution:
Ensure that the Payment Service has CORS configured to allow requests from frontend origins.
Adjust CORS settings in index.js as needed.
7. Docker Networking Problems:
o Problem: Services are not communicating correctly within Docker containers.
o Solution:
Verify Docker Compose service names and network settings.
Ensure that services depend on each other correctly using depends_on.
8. Unhandled Exceptions:
o Problem: The Payment Service crashes due to unhandled exceptions.
o Solution:
Review logs to identify the source of the error.
Implement proper error handling in controllers and middleware.
Use try-catch blocks around asynchronous operations.
________________________________________
9. Next Steps
With Stage 11: Payment & Payouts Integration completed, your project now supports robust payment processing and vendor payouts, enhancing both customer and vendor experiences. This integration ensures secure and flexible financial transactions, essential for a successful eCommerce platform.
Proceed to Stage 12: Inventory Management Integration
In the next stage, we'll focus on integrating inventory management to track product stock levels, manage restocking processes, and synchronize inventory data across microservices.
________________________________________
10. Additional Resources
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• Mongoose Documentation: https://mongoosejs.com/docs/guide.html
• PayPal Developer Documentation: https://developer.paypal.com/docs/api/overview/
• Stripe Developer Documentation: https://stripe.com/docs/development
• RabbitMQ Tutorials: https://www.rabbitmq.com/getstarted.html
• JWT Documentation: https://jwt.io/introduction/
• Winston Documentation: https://github.com/winstonjs/winston
• Swagger Documentation: https://swagger.io/docs/specification/about/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
• PayPal Node.js SDK: https://github.com/paypal/PayPal-node-SDK
• Stripe Node.js SDK: https://github.com/stripe/stripe-node
________________________________________
Disclaimer: This guide provides a foundational setup for the Payment Service within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing more advanced logging mechanisms, integrating additional payment gateways, or enhancing security measures.
________________________________________
By following this comprehensive guide for Stage 11: Payment & Payouts Integration, you've successfully implemented a robust payment processing system that supports multiple payment methods, automates vendor payouts, and ensures secure financial transactions. This setup not only enhances the functionality of your eCommerce platform but also lays the groundwork for future scalability and feature additions.
Next Steps:
Proceed to Stage 12: Inventory Management Integration to further refine your backend services, ensuring efficient stock tracking and synchronization across your platform.
Stage 12: Shipping & Order Management Integration is essential for creating a seamless and efficient eCommerce platform. This stage focuses on integrating shipping functionalities, enhancing order management, and ensuring smooth communication between services. We'll cover setting up a new Shipping Service, updating the Order Service to handle shipping information, integrating with shipping providers, and ensuring robust transaction logging and security protocols.
This comprehensive guide is designed with beginners in mind, providing detailed instructions, complete code snippets, dependencies, configuration files, and documentation to facilitate easy integration. Each step builds upon the previous stages, ensuring that dependencies and interactions are correctly handled. By the end of this stage, you'll have a complete, deployable solution where the developer only needs to upload images, content, and perform minor configuration adjustments.
________________________________________
Stage 12: Shipping & Order Management Integration
________________________________________
1. Overview
Objective:
• Integrate Shipping Services: Implement functionalities for shipping options, rate calculations, label creation, and shipment tracking.
• Enhance Order Management: Update the Order Service to handle shipping information and statuses.
• Integrate with Shipping Providers: Connect with third-party shipping APIs (e.g., Shippo, EasyPost) for real-time shipping data.
• Implement Transaction Logging and Security Protocols: Ensure all shipping and order transactions are securely logged and protected.
Benefits:
• Improved Customer Experience: Offering diverse shipping options and real-time tracking enhances customer satisfaction.
• Efficient Order Processing: Automating shipping processes reduces manual workload and errors.
• Scalability: A dedicated Shipping Service facilitates future expansions and integrations with additional shipping providers.
• Robust Security: Ensuring secure transactions protects sensitive customer and order data.
________________________________________
2. Technology Stack
• Runtime Environment: Node.js v14 or higher
• Framework: Express.js – For building the RESTful API
• Database: MongoDB – For storing shipping and order data
• ODM: Mongoose – For MongoDB object modeling
• Shipping Providers: Shippo or EasyPost – For shipping functionalities
• Message Broker: RabbitMQ – For event-driven communication
• Authentication: JWT – For securing API endpoints
• Logging: Winston – For logging application events
• Configuration Management: dotenv – For managing environment variables
• Testing: Jest and Supertest – For unit and integration testing
• Version Control: Git – Continuation from Stage 8
• Documentation: Markdown and Swagger – For API documentation
________________________________________
3. Implementation Steps
3.1. Setting Up the Shipping Service Directory
We'll create a new microservice called Shipping Service responsible for handling all shipping-related operations.
1. Navigate to the Root Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Create the Shipping Service Workspace:
bash
Copy code
mkdir shipping-service
3. Initialize a New Node.js Project:
bash
Copy code
cd shipping-service
npm init -y
4. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv jsonwebtoken amqplib winston shippo
Note: If you prefer using EasyPost, replace shippo with easypost.
5. Install Development Dependencies:
bash
Copy code
npm install --save-dev nodemon jest supertest eslint prettier swagger-jsdoc swagger-ui-express
6. Update Root package.json:
Add shipping-service to the workspaces array.
json
Copy code
{
"name": "ibrandidit-microservices",
"version": "1.0.0",
"private": true,
"workspaces": [
"common",
"api-gateway",
"auth-service",
"user-service",
"product-service",
"notification-service",
"frontend",
"order-service",
"payment-service",
"shipping-service"
],
"scripts": {
"start": "docker-compose up --build",
"test": "echo \"Specify individual service tests\"",
"frontend": "npm --workspace frontend start",
"order-service": "npm --workspace order-service start",
"payment-service": "npm --workspace payment-service start",
"shipping-service": "npm --workspace shipping-service start"
},
"dependencies": {},
"devDependencies": {}
}
3.2. Project Structure Overview
The shipping-service/ directory will have the following structure:
lua
Copy code
shipping-service/
├── controllers/
│ └── shippingController.js
├── models/
│ └── Shipment.js
├── routes/
│ └── shippingRoutes.js
├── services/
│ ├── shippoService.js
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ └── shipping.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── package.json
├── README.md
├── swagger.js
└── index.js
Explanation:
• controllers/: Contains business logic for handling shipping requests.
• models/: Defines the Mongoose schema for shipments.
• routes/: Defines API endpoints and associates them with controllers.
• services/: Handles external services like Shippo and RabbitMQ.
• middleware/: Contains middleware for authentication and error handling.
• config/: Manages configuration, such as database connections.
• tests/: Contains unit and integration tests.
• .env.example: Template for environment variables.
• .gitignore: Specifies files and directories to ignore in Git.
• jest.config.js: Configuration for Jest testing framework.
• README.md: Documentation for the Shipping Service.
• swagger.js: Configures Swagger for API documentation.
• index.js: Entry point of the application.
3.3. Developing the Shipping Service Components
3.3.1. Configuration Files
1. .gitignore
gitignore
Copy code
# Node modules
node_modules/
# Logs
logs
*.log
npm-debug.log*
# Environment variables
.env
# Coverage reports
coverage/
# OS files
.DS_Store
Thumbs.db
2. .env.example
env
Copy code
PORT=5006
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# Shippo Configuration
SHIPPO_API_TOKEN=your_shippo_api_token
Descriptions:
o PORT: Port on which the Shipping Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
o SHIPPO_API_TOKEN: API token for Shippo integration.
Note: If using EasyPost, replace Shippo variables with EasyPost equivalents.
3. jest.config.js
javascript
Copy code
// shipping-service/jest.config.js
module.exports = {
testEnvironment: 'node',
verbose: true,
setupFilesAfterEnv: ['./jest.setup.js'],
coverageDirectory: './coverage',
collectCoverageFrom: [
'controllers/**/*.js',
'models/**/*.js',
'routes/**/*.js',
'middleware/**/*.js',
'services/**/*.js'
]
};
4. jest.setup.js
(Create this file in the root of shipping-service/)
javascript
Copy code
// shipping-service/jest.setup.js
jest.setTimeout(30000); // Increase timeout if necessary
5. jsdoc.json
json
Copy code
// shipping-service/jsdoc.json
{
"source": {
"include": ["./routes", "./controllers", "./models", "./middleware"],
"exclude": ["node_modules"],
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
}
}
}
3.3.2. Database Configuration
1. config/db.js
javascript
Copy code
// shipping-service/config/db.js
const mongoose = require('mongoose');
const winston = require('winston');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
winston.info('Shipping Service connected to MongoDB');
} catch (error) {
winston.error('Shipping Service failed to connect to MongoDB:', error);
process.exit(1);
}
};
module.exports = connectDB;
3.3.3. Models
1. models/Shipment.js
javascript
Copy code
// shipping-service/models/Shipment.js
const mongoose = require('mongoose');
const ShipmentSchema = new mongoose.Schema({
orderId: {
type: String,
required: true,
ref: 'Order', // Reference to Order model in Order Service
},
shipmentId: {
type: String,
required: true,
},
trackingNumber: {
type: String,
required: true,
},
carrier: {
type: String,
required: true,
},
status: {
type: String,
enum: ['Label Created', 'In Transit', 'Delivered', 'Returned', 'Cancelled'],
default: 'Label Created',
},
shippingCost: {
type: Number,
required: true,
min: 0,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
ShipmentSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Shipment', ShipmentSchema);
3.3.4. Controllers
1. controllers/shippingController.js
javascript
Copy code
// shipping-service/controllers/shippingController.js
const Shipment = require('../models/Shipment');
const shippoService = require('../services/shippoService');
const rabbitmqService = require('../services/rabbitmqService');
const winston = require('winston');
/**
* Create a new shipment
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const createShipment = async (req, res) => {
try {
const { orderId, addressFrom, addressTo, parcel } = req.body;
if (!orderId || !addressFrom || !addressTo || !parcel) {
return res.status(400).json({ message: 'Invalid shipment data' });
}
// Create shipment via Shippo
const shipmentData = await shippoService.createShipment(orderId, addressFrom, addressTo, parcel);
// Save shipment details to database
const shipment = new Shipment({
orderId,
shipmentId: shipmentData.id,
trackingNumber: shipmentData.tracking_number,
carrier: shipmentData.tracking_carrier,
status: shipmentData.status,
shippingCost: shipmentData.amount_total,
});
const savedShipment = await shipment.save();
// Publish shipment created event to RabbitMQ
await rabbitmqService.publishEvent('shipment_created', {
shipmentId: savedShipment._id,
orderId: savedShipment.orderId,
trackingNumber: savedShipment.trackingNumber,
carrier: savedShipment.carrier,
shippingCost: savedShipment.shippingCost,
});
res.status(201).json(savedShipment);
} catch (error) {
winston.error('Error creating shipment:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Get shipment by ID
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getShipmentById = async (req, res) => {
try {
const { id } = req.params;
const shipment = await Shipment.findById(id);
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
res.status(200).json(shipment);
} catch (error) {
winston.error('Error fetching shipment:', error);
res.status(500).json({ message: 'Server error' });
}
};
/**
* Update shipment status
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const updateShipmentStatus = async (req, res) => {
try {
const { id } = req.params;
const { status } = req.body;
const validStatuses = ['Label Created', 'In Transit', 'Delivered', 'Returned', 'Cancelled'];
if (!validStatuses.includes(status)) {
return res.status(400).json({ message: 'Invalid status value' });
}
const shipment = await Shipment.findById(id);
if (!shipment) {
return res.status(404).json({ message: 'Shipment not found' });
}
shipment.status = status;
const updatedShipment = await shipment.save();
// Publish shipment status updated event to RabbitMQ
await rabbitmqService.publishEvent('shipment_status_updated', {
shipmentId: updatedShipment._id,
status: updatedShipment.status,
});
res.status(200).json(updatedShipment);
} catch (error) {
winston.error('Error updating shipment status:', error);
res.status(500).json({ message: 'Server error' });
}
};
module.exports = {
createShipment,
getShipmentById,
updateShipmentStatus,
};
3.3.5. Routes
1. routes/shippingRoutes.js
javascript
Copy code
// shipping-service/routes/shippingRoutes.js
const express = require('express');
const router = express.Router();
const shippingController = require('../controllers/shippingController');
const authMiddleware = require('../middleware/authMiddleware');
/**
* @swagger
* tags:
* name: Shipping
* description: Shipping management
*/
/**
* @swagger
* /shipping:
* post:
* summary: Create a new shipment
* tags: [Shipping]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* orderId:
* type: string
* addressFrom:
* type: object
* properties:
* name:
* type: string
* street1:
* type: string
* city:
* type: string
* state:
* type: string
* zip:
* type: string
* country:
* type: string
* addressTo:
* type: object
* properties:
* name:
* type: string
* street1:
* type: string
* city:
* type: string
* state:
* type: string
* zip:
* type: string
* country:
* type: string
* parcel:
* type: object
* properties:
* length:
* type: number
* width:
* type: number
* height:
* type: number
* distance_unit:
* type: string
* enum: ['in', 'cm']
* weight:
* type: number
* mass_unit:
* type: string
* enum: ['oz', 'g']
* required:
* - orderId
* - addressFrom
* - addressTo
* - parcel
* example:
* orderId: '60d5f9f9b1e8e1d4c8e4f8c5'
* addressFrom:
* name: 'Warehouse'
* street1: '1234 Main St'
* city: 'Anytown'
* state: 'CA'
* zip: '12345'
* country: 'US'
* addressTo:
* name: 'Customer'
* street1: '5678 Market St'
* city: 'Othertown'
* state: 'NY'
* zip: '67890'
* country: 'US'
* parcel:
* length: 10
* width: 5
* height: 8
* distance_unit: 'in'
* weight: 2
* mass_unit: 'oz'
* responses:
* 201:
* description: Shipment created successfully
* 400:
* description: Invalid shipment data
* 401:
* description: Unauthorized
* 500:
* description: Server error
*/
router.post('/', authMiddleware, shippingController.createShipment);
/**
* @swagger
* /shipping/{id}:
* get:
* summary: Get shipment by ID
* tags: [Shipping]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: string
* required: true
* description: Shipment ID
* responses:
* 200:
* description: Shipment details
* 401:
* description: Unauthorized
* 404:
* description: Shipment not found
* 500:
* description: Server error
*/
router.get('/:id', authMiddleware, shippingController.getShipmentById);
/**
* @swagger
* /shipping/{id}/status:
* put:
* summary: Update shipment status
* tags: [Shipping]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* schema:
* type: string
* required: true
* description: Shipment ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* enum: ['Label Created', 'In Transit', 'Delivered', 'Returned', 'Cancelled']
* required:
* - status
* example:
* status: 'In Transit'
* responses:
* 200:
* description: Shipment status updated successfully
* 400:
* description: Invalid status value
* 401:
* description: Unauthorized
* 404:
* description: Shipment not found
* 500:
* description: Server error
*/
router.put('/:id/status', authMiddleware, shippingController.updateShipmentStatus);
module.exports = router;
3.3.6. Middleware
1. middleware/authMiddleware.js
javascript
Copy code
// shipping-service/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const winston = require('winston');
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Authorization token missing or malformed' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
winston.error('Invalid token:', error);
return res.status(401).json({ message: 'Invalid or expired token' });
}
};
module.exports = authMiddleware;
2. middleware/errorHandler.js
javascript
Copy code
// shipping-service/middleware/errorHandler.js
const winston = require('winston');
const errorHandler = (err, req, res, next) => {
winston.error(err.message, err);
res.status(500).json({ message: 'Server error' });
};
module.exports = errorHandler;
3.3.7. Services
1. services/shippoService.js
javascript
Copy code
// shipping-service/services/shippoService.js
const shippo = require('shippo')(`${process.env.SHIPPO_API_TOKEN}`);
const winston = require('winston');
/**
* Create a shipment using Shippo
* @param {string} orderId - Order ID
* @param {Object} addressFrom - Sender address
* @param {Object} addressTo - Recipient address
* @param {Object} parcel - Parcel details
* @returns {Promise<Object>} Shippo shipment object
*/
const createShipment = async (orderId, addressFrom, addressTo, parcel) => {
try {
const shipment = await shippo.shipment.create({
address_from: addressFrom,
address_to: addressTo,
parcels: [parcel],
async: false,
});
if (shipment.status === 'VALID') {
// Purchase label for the first rate
const rate = shipment.rates[0];
const purchasedRate = await shippo.transaction.create({
rate: rate.object_id,
label_file_type: 'PDF',
async: false,
});
if (purchasedRate.status === 'SUCCESS') {
return {
id: shipment.object_id,
tracking_number: purchasedRate.tracking_number,
tracking_carrier: purchasedRate.tracking_carrier,
status: 'Label Created',
amount_total: purchasedRate.amount,
};
} else {
throw new Error('Failed to purchase shipping label');
}
} else {
throw new Error('Invalid shipment details');
}
} catch (error) {
winston.error('Error creating shipment with Shippo:', error);
throw error;
}
};
module.exports = {
createShipment,
};
2. services/rabbitmqService.js
javascript
Copy code
// shipping-service/services/rabbitmqService.js
const amqp = require('amqplib');
const winston = require('winston');
let channel = null;
const connectRabbitMQ = async () => {
try {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
winston.info('Shipping Service connected to RabbitMQ');
} catch (error) {
winston.error('Shipping Service failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
const publishEvent = async (event, data) => {
if (!channel) {
await connectRabbitMQ();
}
const queue = 'shipping_events';
await channel.assertQueue(queue, { durable: true });
const payload = { event, data };
channel.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
winston.info(`Published event to RabbitMQ: ${event}`);
};
connectRabbitMQ();
module.exports = {
publishEvent,
};
3.3.8. Controllers
(Already provided above in controllers/shippingController.js)
3.3.9. Routes
(Already provided above in routes/shippingRoutes.js)
3.3.10. Entry Point
1. index.js
javascript
Copy code
// shipping-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const bodyParser = require('body-parser');
const winston = require('winston');
const connectDB = require('./config/db');
const shippingRoutes = require('./routes/shippingRoutes');
const errorHandler = require('./middleware/errorHandler');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Logging configuration
winston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
// Connect to MongoDB
connectDB();
// Routes
app.use('/shipping', shippingRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Error Handling Middleware
app.use(errorHandler);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Shipping Service API');
});
// Start Server
const PORT = process.env.PORT || 5006;
app.listen(PORT, () => {
winston.info(`Shipping Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3.3.11. Swagger Configuration
1. swagger.js
javascript
Copy code
// shipping-service/swagger.js
const swaggerJSDoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'iBranDIDit.com Shipping Service API',
version: '1.0.0',
description: 'API documentation for the Shipping Service of iBranDIDit.com eCommerce platform',
},
servers: [
{
url: 'http://localhost:5006',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
schemas: {
ShipmentInput: {
type: 'object',
required: ['orderId', 'addressFrom', 'addressTo', 'parcel'],
properties: {
orderId: {
type: 'string',
description: 'Order ID referencing the Order model',
},
addressFrom: {
type: 'object',
properties: {
name: { type: 'string' },
street1: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
zip: { type: 'string' },
country: { type: 'string' },
},
description: 'Sender address',
},
addressTo: {
type: 'object',
properties: {
name: { type: 'string' },
street1: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
zip: { type: 'string' },
country: { type: 'string' },
},
description: 'Recipient address',
},
parcel: {
type: 'object',
properties: {
length: { type: 'number' },
width: { type: 'number' },
height: { type: 'number' },
distance_unit: { type: 'string', enum: ['in', 'cm'] },
weight: { type: 'number' },
mass_unit: { type: 'string', enum: ['oz', 'g'] },
},
description: 'Parcel details',
},
},
example: {
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
addressFrom: {
name: 'Warehouse',
street1: '1234 Main St',
city: 'Anytown',
state: 'CA',
zip: '12345',
country: 'US',
},
addressTo: {
name: 'Customer',
street1: '5678 Market St',
city: 'Othertown',
state: 'NY',
zip: '67890',
country: 'US',
},
parcel: {
length: 10,
width: 5,
height: 8,
distance_unit: 'in',
weight: 2,
mass_unit: 'oz',
},
},
},
ShipmentOutput: {
type: 'object',
properties: {
_id: { type: 'string', description: 'Shipment ID' },
orderId: { type: 'string', description: 'Order ID' },
shipmentId: { type: 'string', description: 'Shippo Shipment ID' },
trackingNumber: { type: 'string', description: 'Tracking Number' },
carrier: { type: 'string', description: 'Shipping Carrier' },
status: {
type: 'string',
enum: ['Label Created', 'In Transit', 'Delivered', 'Returned', 'Cancelled'],
description: 'Current status of the shipment',
},
shippingCost: { type: 'number', description: 'Cost of shipping' },
createdAt: { type: 'string', format: 'date-time', description: 'Shipment creation timestamp' },
updatedAt: { type: 'string', format: 'date-time', description: 'Shipment update timestamp' },
},
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./routes/*.js', './controllers/*.js'],
};
const swaggerSpec = swaggerJSDoc(options);
module.exports = swaggerSpec;
Explanation:
o ShipmentInput: Schema for creating a new shipment.
o ShipmentOutput: Schema for the shipment response.
2. Annotate Routes with Swagger Comments
(Already provided in routes/shippingRoutes.js above)
3.3.12. Testing
1. tests/shipping.test.js
javascript
Copy code
// shipping-service/tests/shipping.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const app = require('../index');
const Shipment = require('../models/Shipment');
const jwt = require('jsonwebtoken');
// Generate a test JWT token
const testUser = {
id: '60d5f9f9b1e8e1d4c8e4f8c5',
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(testUser, process.env.JWT_SECRET, { expiresIn: '1h' });
beforeAll(async () => {
// Connect to the test database
await mongoose.connect(process.env.MONGODB_URI_TEST || 'mongodb://localhost:27017/shipping_test', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
// Clean up database after tests
await Shipment.deleteMany({});
await mongoose.connection.close();
});
describe('Shipping API Endpoints', () => {
it('should create a new shipment', async () => {
const res = await request(app)
.post('/shipping')
.set('Authorization', `Bearer ${token}`)
.send({
orderId: '60d5f9f9b1e8e1d4c8e4f8c5',
addressFrom: {
name: 'Warehouse',
street1: '1234 Main St',
city: 'Anytown',
state: 'CA',
zip: '12345',
country: 'US',
},
addressTo: {
name: 'Customer',
street1: '5678 Market St',
city: 'Othertown',
state: 'NY',
zip: '67890',
country: 'US',
},
parcel: {
length: 10,
width: 5,
height: 8,
distance_unit: 'in',
weight: 2,
mass_unit: 'oz',
},
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body).toHaveProperty('orderId', '60d5f9f9b1e8e1d4c8e4f8c5');
expect(res.body).toHaveProperty('trackingNumber');
expect(res.body).toHaveProperty('carrier');
expect(res.body).toHaveProperty('status', 'Label Created');
expect(res.body).toHaveProperty('shippingCost');
});
it('should not create shipment with invalid data', async () => {
const res = await request(app)
.post('/shipping')
.set('Authorization', `Bearer ${token}`)
.send({
orderId: '',
addressFrom: {},
addressTo: {},
parcel: {},
});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid shipment data');
});
it('should retrieve a shipment by ID', async () => {
const shipment = await Shipment.findOne();
const res = await request(app)
.get(`/shipping/${shipment._id}`)
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('_id', shipment._id.toString());
});
it('should update shipment status', async () => {
const shipment = await Shipment.findOne();
const res = await request(app)
.put(`/shipping/${shipment._id}/status`)
.set('Authorization', `Bearer ${token}`)
.send({ status: 'In Transit' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('status', 'In Transit');
});
it('should not update shipment status with invalid status', async () => {
const shipment = await Shipment.findOne();
const res = await request(app)
.put(`/shipping/${shipment._id}/status`)
.set('Authorization', `Bearer ${token}`)
.send({ status: 'UnknownStatus' });
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Invalid status value');
});
it('should return 404 for non-existent shipment', async () => {
const res = await request(app)
.get('/shipping/60c72b2f9b1e8e1d4c8e4f8d') // Assuming this ID doesn't exist
.set('Authorization', `Bearer ${token}`);
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Shipment not found');
});
});
3.3.13. Logging Configuration
The service uses Winston for logging. Configuration is included in index.js and other parts as needed.
3.3.14. README Documentation
1. README.md
markdown
Copy code
# Shipping Service
## Overview
The **Shipping Service** is a microservice responsible for managing all shipping-related operations in the iBranDIDit.com eCommerce platform. It handles the creation of shipping labels, calculates shipping rates, tracks shipments, and updates shipment statuses. The service integrates with third-party shipping providers like Shippo to facilitate real-time shipping data and automates communication with other services via RabbitMQ.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Logging](#logging)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [MongoDB](https://www.mongodb.com/) (local or hosted)
- [RabbitMQ](https://www.rabbitmq.com/) (local or hosted)
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- [Shippo Developer Account](https://goshippo.com/) for API credentials
## Installation
1. **Navigate to the Shipping Service Directory:**
```bash
cd shipping-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the shipping-service/ directory based on the provided .env.example.
env
Copy code
PORT=5006
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# Shippo Configuration
SHIPPO_API_TOKEN=your_shippo_api_token
Descriptions:
o PORT: Port on which the Shipping Service runs.
o NODE_ENV: Environment mode (development, production).
o MONGODB_URI: MongoDB connection string.
o JWT_SECRET: Secret key for JWT authentication.
o RABBITMQ_URL: Connection string for RabbitMQ.
o SHIPPO_API_TOKEN: API token for Shippo integration.
Note: If using EasyPost, replace Shippo variables with EasyPost equivalents.
Running the Service
Option 1: Using Docker Compose
Ensure Docker and Docker Compose are installed.
3. Start the Service:
bash
Copy code
docker-compose up --build shipping-service
Note: Ensure that MongoDB and RabbitMQ services are also running, either via the root docker-compose.yml or individually.
Option 2: Running Locally
4. Start MongoDB and RabbitMQ:
Ensure that MongoDB and RabbitMQ services are running. You can use Docker Compose to start them:
bash
Copy code
docker-compose up -d mongodb rabbitmq
5. Start the Shipping Service:
bash
Copy code
npm run dev
Note: The dev script uses nodemon for automatic restarts on code changes.
API Documentation
Access the Swagger UI for detailed API documentation:
o URL: http://localhost:5006/api-docs
Shipping Endpoints
o Create Shipment: POST /shipping
o Get Shipment by ID: GET /shipping/{id}
o Update Shipment Status: PUT /shipping/{id}/status
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
Testing
Testing ensures that the Shipping Service behaves as expected.
6. Run Tests:
bash
Copy code
cd shipping-service
npm test
7. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
Logging
The Shipping Service uses Winston for logging. Logs are output to the console in a simple format. You can configure additional transports (e.g., file, external logging services) by modifying the Winston configuration in index.js.
Contribution
Contributions are welcome! Please adhere to the following guidelines:
8. Fork the Repository:
Create a personal fork of the Shipping Service repository.
9. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/add-shipment-endpoint
10. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(shipping-service): add shipment creation endpoint"
11. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-shipment-endpoint
Submit a pull request from your fork to the main repository for review.
License
MIT
________________________________________
Copy code
3.3.15. Dockerization
1. Create Dockerfile
dockerfile
Copy code
# shipping-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 5006
# Start the service
CMD [ "npm", "start" ]
2. Update Root docker-compose.yml
Ensure that the Shipping Service is included in the Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
# ... other services
volumes:
mongo-data:
Explanation:
o shipping-service: Configured to build from the shipping-service directory, expose port 5006, and link to MongoDB and RabbitMQ services.
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Shipping Service will be accessible at http://localhost:5006/.
o Swagger UI for Shipping Service: http://localhost:5006/api-docs.
________________________________________
4. Complete Code and Configuration Files
4.1. Shipping Service package.json
json
Copy code
{
"name": "shipping-service",
"version": "1.0.0",
"description": "Shipping Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"shippo": "^1.5.0",
"winston": "^3.8.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
},
"jest": {
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverageFrom": [
"controllers/**/*.js",
"models/**/*.js",
"routes/**/*.js",
"middleware/**/*.js",
"services/**/*.js"
]
},
"config": {
"jsdoc": {
"opts": {
"destination": "./docs",
"recurse": true,
"template": "node_modules/jsdoc-template"
}
}
}
}
4.2. Shipping Service Directory Structure
lua
Copy code
shipping-service/
├── controllers/
│ └── shippingController.js
├── models/
│ └── Shipment.js
├── routes/
│ └── shippingRoutes.js
├── services/
│ ├── shippoService.js
│ └── rabbitmqService.js
├── middleware/
│ ├── authMiddleware.js
│ └── errorHandler.js
├── config/
│ └── db.js
├── tests/
│ └── shipping.test.js
├── .env.example
├── .gitignore
├── jest.config.js
├── jsdoc.json
├── package.json
├── README.md
├── swagger.js
└── index.js
4.3. Sample Component Files
4.3.1. controllers/shippingController.js
(Already provided above)
4.3.2. models/Shipment.js
(Already provided above)
4.3.3. routes/shippingRoutes.js
(Already provided above)
4.3.4. middleware/authMiddleware.js
(Already provided above)
4.3.5. middleware/errorHandler.js
(Already provided above)
4.3.6. services/shippoService.js
(Already provided above)
4.3.7. services/rabbitmqService.js
(Already provided above)
4.3.8. config/db.js
(Already provided above)
4.3.9. swagger.js
(Already provided above)
4.3.10. index.js
(Already provided above)
4.3.11. README.md
(Already provided above)
________________________________________
5. Documentation and Usage
5.1. Running the Shipping Service Locally
1. Ensure MongoDB and RabbitMQ Are Running:
If you're using Docker Compose for all services, ensure that MongoDB and RabbitMQ are up.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d mongodb rabbitmq
2. Start the Shipping Service:
bash
Copy code
cd shipping-service
npm run dev
o The service will run on http://localhost:5006/.
o Swagger UI for Shipping Service: http://localhost:5006/api-docs.
5.2. API Endpoints
Access the Swagger UI to explore and test the API endpoints.
• Shipping Endpoints:
o Create Shipment: POST /shipping
o Get Shipment by ID: GET /shipping/{id}
o Update Shipment Status: PUT /shipping/{id}/status
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
5.3. Testing
Testing ensures that the Shipping Service behaves as expected.
1. Run Tests:
bash
Copy code
cd shipping-service
npm test
2. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
5.4. Linting and Formatting
1. Run Linting:
bash
Copy code
npm run lint
2. Run Formatting:
bash
Copy code
npm run format
5.5. Generating Documentation
1. Annotate Code with JSDoc Comments:
Ensure that all controllers, models, and routes are properly documented with JSDoc.
javascript
Copy code
// shipping-service/controllers/shippingController.js
/**
* Create a new shipment
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Object} JSON response with shipment details
*/
const createShipment = async (req, res) => { /* ... */ };
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
5.6. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the Shipping Service repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/add-shipment-endpoint
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(shipping-service): add shipment creation endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-shipment-endpoint
o Submit a pull request from your fork to the main repository for review.
________________________________________
6. Integration with Previous Services
6.1. Updating Order Service
To handle shipping information and statuses, update the Order Service to include shipping-related fields.
1. Navigate to Order Service Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/order-service
2. Update Order Model (models/Order.js):
javascript
Copy code
// order-service/models/Order.js
const mongoose = require('mongoose');
const OrderSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'User', // Reference to User model in User Service
},
products: [
{
productId: {
type: String,
required: true,
ref: 'Product', // Reference to Product model in Product Service
},
quantity: {
type: Number,
required: true,
min: 1,
},
price: {
type: Number,
required: true,
min: 0,
},
},
],
totalAmount: {
type: Number,
required: true,
min: 0,
},
status: {
type: String,
enum: ['Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled'],
default: 'Pending',
},
shipment: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Shipment',
required: false,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
OrderSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Order', OrderSchema);
3. Update Order Controller to Handle Shipping (controllers/orderController.js):
(Assuming you have a controller for orders)
javascript
Copy code
// order-service/controllers/orderController.js
const Order = require('../models/Order');
const Shipment = require('../../shipping-service/models/Shipment'); // Adjust path as needed
const rabbitmqService = require('../services/rabbitmqService');
const winston = require('winston');
/**
* Update order with shipment details
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const updateOrderWithShipment = async (req, res) => {
try {
const { orderId, shipmentId } = req.body;
if (!orderId || !shipmentId) {
return res.status(400).json({ message: 'Invalid data' });
}
const order = await Order.findById(orderId);
const shipment = await Shipment.findById(shipmentId);
if (!order || !shipment) {
return res.status(404).json({ message: 'Order or Shipment not found' });
}
order.shipment = shipmentId;
order.status = 'Shipped';
await order.save();
// Publish order shipped event
await rabbitmqService.publishEvent('order_shipped', {
orderId: order._id,
shipmentId: shipment._id,
trackingNumber: shipment.trackingNumber,
});
res.status(200).json({ message: 'Order updated with shipment details', order });
} catch (error) {
winston.error('Error updating order with shipment:', error);
res.status(500).json({ message: 'Server error' });
}
};
module.exports = {
// ... other controllers
updateOrderWithShipment,
};
4. Update Order Routes (routes/orderRoutes.js):
javascript
Copy code
// order-service/routes/orderRoutes.js
const express = require('express');
const router = express.Router();
const orderController = require('../controllers/orderController');
const authMiddleware = require('../middleware/authMiddleware');
// ... existing routes
/**
* @swagger
* /orders/shipment:
* put:
* summary: Update order with shipment details
* tags: [Orders]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* orderId:
* type: string
* shipmentId:
* type: string
* required:
* - orderId
* - shipmentId
* example:
* orderId: '60d5f9f9b1e8e1d4c8e4f8c5'
* shipmentId: '60d5f9f9b1e8e1d4c8e4f8c8'
* responses:
* 200:
* description: Order updated with shipment details
* 400:
* description: Invalid data
* 401:
* description: Unauthorized
* 404:
* description: Order or Shipment not found
* 500:
* description: Server error
*/
router.put('/shipment', authMiddleware, orderController.updateOrderWithShipment);
module.exports = router;
3.4. Integrating with RabbitMQ
Ensure that the Shipping Service publishes relevant events to RabbitMQ and that the Order Service consumes these events to update order statuses accordingly.
1. Shipping Service Publishing Events:
(Already handled in shippingController.js via publishEvent calls)
2. Order Service Consuming Events:
1. Create a RabbitMQ Consumer in Order Service:
javascript
Copy code
// order-service/services/rabbitmqConsumer.js
const amqp = require('amqplib');
const Order = require('../models/Order');
const winston = require('winston');
const connectRabbitMQ = async () => {
try {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
const queue = 'shipping_events';
await channel.assertQueue(queue, { durable: true });
channel.prefetch(1);
winston.info(`Order Service waiting for messages in ${queue}`);
channel.consume(queue, async (msg) => {
if (msg !== null) {
const payload = JSON.parse(msg.content.toString());
const { event, data } = payload;
if (event === 'shipment_created') {
const { orderId, shipmentId, trackingNumber, carrier, shippingCost } = data;
const order = await Order.findById(orderId);
if (order) {
order.shipment = shipmentId;
order.status = 'Shipped';
await order.save();
winston.info(`Order ${orderId} updated to Shipped`);
// Acknowledge the message
channel.ack(msg);
} else {
winston.warn(`Order ${orderId} not found`);
channel.nack(msg, false, false); // Discard the message
}
}
// Handle other events as needed
}
});
} catch (error) {
winston.error('Order Service failed to connect to RabbitMQ:', error);
setTimeout(connectRabbitMQ, 5000); // Retry after 5 seconds
}
};
connectRabbitMQ();
2. Update Root index.js or a Dedicated File in Order Service:
Ensure that the consumer is started when the Order Service starts.
javascript
Copy code
// order-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const bodyParser = require('body-parser');
const winston = require('winston');
const connectDB = require('./config/db');
const orderRoutes = require('./routes/orderRoutes');
const errorHandler = require('./middleware/errorHandler');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger');
const rabbitmqConsumer = require('./services/rabbitmqConsumer'); // Import the consumer
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Logging configuration
winston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
// Connect to MongoDB
connectDB();
// Routes
app.use('/orders', orderRoutes);
// Swagger Documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// Error Handling Middleware
app.use(errorHandler);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Order Service API');
});
// Start Server
const PORT = process.env.PORT || 5004;
app.listen(PORT, () => {
winston.info(`Order Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3.5. Testing
Testing ensures that the Shipping Service and its integration with the Order Service behave as expected.
3.5.1. Unit Tests
• tests/shipping.test.js
(Already provided above)
3.5.2. Integration Tests
Integration tests can be expanded to handle real API calls and message queue interactions. For simplicity, the provided tests focus on API endpoints. For comprehensive testing, consider mocking external services like Shippo and RabbitMQ.
3.6. Linting and Formatting
Maintaining code quality is crucial, especially as the project scales.
1. Initialize ESLint:
bash
Copy code
npx eslint --init
o Choose the following options:
How would you like to use ESLint? To check syntax, find problems, and enforce code style
What type of modules does your project use? CommonJS
Which framework does your project use? None of these
Does your project use TypeScript? No
Where does your code run? Node
How would you like to define a style for your project? Use a popular style guide
Which style guide do you want to follow? Airbnb
What format do you want your config file to be in? JavaScript
Would you like to install them now with npm? Yes
2. Configure Prettier:
Create a .prettierrc file in shipping-service/:
json
Copy code
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}
3. Add Lint and Format Scripts:
json
Copy code
// shipping-service/package.json
{
// ...
"scripts": {
// ...
"lint": "eslint . --ext .js",
"format": "prettier --write ."
},
// ...
}
4. Run Linting and Formatting:
bash
Copy code
npm run lint
npm run format
3.7. Dockerization
Containerizing the Shipping Service ensures consistent environments across development, testing, and production.
1. Create Dockerfile
(Already provided above)
2. Update Root docker-compose.yml
(Already provided above)
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Shipping Service will be accessible at http://localhost:5006/.
o Swagger UI for Shipping Service: http://localhost:5006/api-docs.
________________________________________
7. Documentation and Usage
7.1. Running the Shipping Service Locally
1. Ensure MongoDB and RabbitMQ Are Running:
If you're using Docker Compose for all services, ensure that MongoDB and RabbitMQ are up.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d mongodb rabbitmq
2. Start the Shipping Service:
bash
Copy code
cd shipping-service
npm run dev
o The service will run on http://localhost:5006/.
o Swagger UI for Shipping Service: http://localhost:5006/api-docs.
7.2. API Endpoints
Access the Swagger UI to explore and test the API endpoints.
• Shipping Endpoints:
o Create Shipment: POST /shipping
o Get Shipment by ID: GET /shipping/{id}
o Update Shipment Status: PUT /shipping/{id}/status
Authentication:
All endpoints are protected and require a valid JWT token in the Authorization header.
Example:
makefile
Copy code
Authorization: Bearer your_jwt_token
7.3. Testing
Testing ensures that the Shipping Service and its integration with the Order Service behave as expected.
1. Run Tests:
bash
Copy code
cd shipping-service
npm test
2. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
7.4. Linting and Formatting
1. Run Linting:
bash
Copy code
npm run lint
2. Run Formatting:
bash
Copy code
npm run format
7.5. Generating Documentation
1. Annotate Code with JSDoc Comments:
Ensure that all controllers, models, and routes are properly documented with JSDoc.
javascript
Copy code
// shipping-service/controllers/shippingController.js
/**
* Create a new shipment
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Object} JSON response with shipment details
*/
const createShipment = async (req, res) => { /* ... */ };
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
7.6. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the Shipping Service repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/add-shipment-endpoint
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(shipping-service): add shipment creation endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/add-shipment-endpoint
o Submit a pull request from your fork to the main repository for review.
________________________________________
8. Troubleshooting Tips
1. Shippo Integration Issues:
o Problem: Unable to create or execute shipments with Shippo.
o Solution:
Verify API credentials in .env.
Ensure that the Shippo SDK is correctly configured.
Check network connectivity to Shippo.
Review logs for detailed error messages.
2. Webhook Handling Failures:
o Problem: Shippo webhooks are not being received or processed.
o Solution:
Ensure that webhook endpoints are publicly accessible (use tools like ngrok during development).
Verify webhook signing secrets and event types.
Check logs for any errors during webhook processing.
3. RabbitMQ Connectivity Problems:
o Problem: Unable to connect to RabbitMQ for event publishing.
o Solution:
Verify RabbitMQ service is running.
Check connection string and credentials in .env.
Ensure network configurations allow connectivity.
4. MongoDB Connection Errors:
o Problem: Shipping Service fails to connect to MongoDB.
o Solution:
Ensure MongoDB is running and accessible at the URI specified in .env.
Check Docker Compose logs for any MongoDB-related errors.
Verify network configurations if MongoDB is hosted externally.
5. Authentication Failures:
o Problem: Receiving 401 Unauthorized errors when accessing protected endpoints.
o Solution:
Ensure that a valid JWT token is being sent in the Authorization header.
Verify that the JWT_SECRET in Shipping Service matches the one used by Auth Service.
Check token expiration and validity.
6. CORS Issues:
o Problem: API calls from frontend are blocked due to CORS policy.
o Solution:
Ensure that the Shipping Service has CORS configured to allow requests from frontend origins.
Adjust CORS settings in index.js as needed.
7. Docker Networking Problems:
o Problem: Services are not communicating correctly within Docker containers.
o Solution:
Verify Docker Compose service names and network settings.
Ensure that services depend on each other correctly using depends_on.
8. Unhandled Exceptions:
o Problem: The Shipping Service crashes due to unhandled exceptions.
o Solution:
Review logs to identify the source of the error.
Implement proper error handling in controllers and middleware.
Use try-catch blocks around asynchronous operations.
________________________________________
9. Additional Resources
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• Mongoose Documentation: https://mongoosejs.com/docs/guide.html
• Shippo Developer Documentation: https://goshippo.com/docs/
• RabbitMQ Tutorials: https://www.rabbitmq.com/getstarted.html
• JWT Documentation: https://jwt.io/introduction/
• Winston Documentation: https://github.com/winstonjs/winston
• Swagger Documentation: https://swagger.io/docs/specification/about/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Supertest Documentation: https://github.com/visionmedia/supertest
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
• Shippo Node.js SDK: https://github.com/goshippo/shippo-node-client
________________________________________
Disclaimer: This guide provides a foundational setup for the Shipping Service within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as integrating additional shipping providers, enhancing security measures, or implementing advanced logging mechanisms.
________________________________________
By following this comprehensive guide for Stage 12: Shipping & Order Management Integration, you've successfully implemented a robust Shipping Service that manages shipping operations, integrates with third-party shipping providers, and communicates seamlessly with the Order Service. This integration enhances both customer and vendor experiences by providing reliable shipping options and real-time tracking capabilities.
Next Steps:
Proceed to Stage 13: Inventory Management Integration to further refine your backend services, ensuring efficient stock tracking and synchronization across your platform.
Stage 13: User Dashboard & Features is a crucial component of your eCommerce platform, providing users with a centralized area to manage their profiles, view orders, track shipments, and more. This stage focuses on building a user-friendly dashboard that interacts seamlessly with the existing microservices developed in previous stages.
This comprehensive guide will walk you through setting up the User Dashboard using React.js, integrating it with backend services such as Auth Service, Order Service, Shipping Service, and Payment Service. We'll cover all necessary code, dependencies, configuration files, and documentation to ensure easy integration, even for beginners.
________________________________________
Stage 13: User Dashboard & Features
________________________________________
1. Overview
Objective:
• Build a User Dashboard Frontend: Create a responsive and intuitive dashboard where users can manage their profiles, view order history, track shipments, manage payment methods, and handle other account-related tasks.
• Integrate with Backend Microservices: Connect the frontend with existing backend services (Auth, User, Order, Payment, Shipping) to fetch and manipulate data.
• Implement Secure Authentication: Ensure that user data is protected through secure authentication mechanisms using JWT.
• Enhance User Experience: Provide features like real-time updates, notifications, and seamless navigation to improve overall user satisfaction.
Benefits:
• Centralized User Management: Allows users to handle all account-related activities in one place.
• Improved Engagement: Enhances user interaction with the platform, leading to higher retention rates.
• Scalability: A well-structured dashboard facilitates the addition of new features in the future.
• Security: Secure authentication and data handling protect user information and maintain trust.
________________________________________
2. Technology Stack
• Frontend Framework: React.js – For building the user interface.
• State Management: Redux – For managing application state.
• Routing: React Router – For handling navigation within the app.
• HTTP Client: Axios – For making API requests to backend services.
• UI Library: Material-UI (MUI) – For pre-built UI components and styling.
• Authentication: JWT (JSON Web Tokens) – For securing API endpoints.
• Form Handling: Formik and Yup – For form management and validation.
• Notifications: React Toastify – For displaying notifications to users.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Version Control: Git – Continuation from previous stages.
• Documentation: Markdown and Swagger – For API documentation.
________________________________________
3. Implementation Steps
3.1. Setting Up the Frontend Project
We'll create a new React.js project for the User Dashboard. If you already have a frontend project, you can integrate the dashboard features into it. For this guide, we'll assume a new project setup for clarity.
1. Navigate to the Root Directory:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Create the Frontend Workspace:
bash
Copy code
mkdir frontend
cd frontend
3. Initialize a New React Project Using Create React App:
bash
Copy code
npx create-react-app user-dashboard
cd user-dashboard
4. Install Additional Dependencies:
bash
Copy code
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled react-router-dom axios redux react-redux @reduxjs/toolkit formik yup react-toastify
Dependencies Breakdown:
o @mui/material, @mui/icons-material, @emotion/react, @emotion/styled: For Material-UI components and styling.
o react-router-dom: For routing and navigation.
o axios: For making HTTP requests.
o redux, react-redux, @reduxjs/toolkit: For state management.
o formik, yup: For form handling and validation.
o react-toastify: For displaying notifications.
5. Set Up Project Structure:
Organize the project directories for better maintainability.
java
Copy code
user-dashboard/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── Layout/
│ │ ├── Dashboard/
│ │ ├── Profile/
│ │ ├── Orders/
│ │ ├── Shipments/
│ │ ├── Payments/
│ │ └── ...
│ ├── redux/
│ │ ├── store.js
│ │ └── slices/
│ ├── pages/
│ │ ├── Home/
│ │ ├── Login/
│ │ ├── Register/
│ │ ├── Dashboard/
│ │ ├── Profile/
│ │ ├── Orders/
│ │ ├── Shipments/
│ │ ├── Payments/
│ │ └── ...
│ ├── services/
│ │ └── api.js
│ ├── utils/
│ ├── App.js
│ ├── index.js
│ └── ...
├── .env
├── package.json
├── README.md
└── ...
3.2. Configuration Files
1. .gitignore
Ensure that sensitive files and directories are ignored by Git.
gitignore
Copy code
# dependencies
/node_modules
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
2. .env
Create a .env file in the user-dashboard/ directory to store environment variables.
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:5004 # Replace with actual Order Service URL
REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
Note: Ensure that the URLs match the ports on which your backend services are running. If using Docker Compose with service names, adjust accordingly.
3. package.json
The package.json will already include the installed dependencies. Ensure the scripts are set up for development and production builds.
json
Copy code
{
"name": "user-dashboard",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.5",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0",
"formik": "^2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.1",
"react-router-dom": "^6.14.1",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.3",
"redux": "^4.2.1",
"yup": "^1.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {}
}
3.3. Redux Store Setup
1. Create Redux Store (src/redux/store.js):
javascript
Copy code
// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import orderReducer from './slices/orderSlice';
import shipmentReducer from './slices/shipmentSlice';
import paymentReducer from './slices/paymentSlice';
const store = configureStore({
reducer: {
auth: authReducer,
orders: orderReducer,
shipments: shipmentReducer,
payments: paymentReducer,
},
});
export default store;
2. Create Auth Slice (src/redux/slices/authSlice.js):
javascript
Copy code
// src/redux/slices/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const initialState = {
user: null,
token: localStorage.getItem('token') || null,
status: 'idle',
error: null,
};
export const login = createAsyncThunk(
'auth/login',
async ({ email, password }, { rejectWithValue }) => {
try {
const response = await axios.post(`${API_BASE_URL}/login`, { email, password });
return response.data;
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
export const register = createAsyncThunk(
'auth/register',
async ({ name, email, password }, { rejectWithValue }) => {
try {
const response = await axios.post(`${API_BASE_URL}/register`, { name, email, password });
return response.data;
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout(state) {
state.user = null;
state.token = null;
localStorage.removeItem('token');
},
setCredentials(state, action) {
const { user, token } = action.payload;
state.user = user;
state.token = token;
localStorage.setItem('token', token);
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(login.pending, (state) => {
state.status = 'loading';
})
.addCase(login.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
localStorage.setItem('token', action.payload.token);
})
.addCase(login.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ? action.payload.message : action.error.message;
})
// Register
.addCase(register.pending, (state) => {
state.status = 'loading';
})
.addCase(register.fulfilled, (state, action) => {
state.status = 'succeeded';
state.user = action.payload.user;
state.token = action.payload.token;
localStorage.setItem('token', action.payload.token);
})
.addCase(register.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ? action.payload.message : action.error.message;
});
},
});
export const { logout, setCredentials } = authSlice.actions;
export default authSlice.reducer;
3. Create Order Slice (src/redux/slices/orderSlice.js):
javascript
Copy code
// src/redux/slices/orderSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const initialState = {
orders: [],
status: 'idle',
error: null,
};
export const fetchOrders = createAsyncThunk(
'orders/fetchOrders',
async (_, { getState, rejectWithValue }) => {
const token = getState().auth.token;
try {
const response = await axios.get(`${API_BASE_URL}/orders`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data.orders;
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
const orderSlice = createSlice({
name: 'orders',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchOrders.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchOrders.fulfilled, (state, action) => {
state.status = 'succeeded';
state.orders = action.payload;
})
.addCase(fetchOrders.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ? action.payload.message : action.error.message;
});
},
});
export default orderSlice.reducer;
4. Create Shipment Slice (src/redux/slices/shipmentSlice.js):
javascript
Copy code
// src/redux/slices/shipmentSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const SHIPPING_SERVICE_URL = process.env.REACT_APP_SHIPPING_SERVICE_URL;
const initialState = {
shipments: [],
status: 'idle',
error: null,
};
export const fetchShipments = createAsyncThunk(
'shipments/fetchShipments',
async (_, { getState, rejectWithValue }) => {
const token = getState().auth.token;
try {
const response = await axios.get(`${SHIPPING_SERVICE_URL}/shipping`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data.shipments;
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
const shipmentSlice = createSlice({
name: 'shipments',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchShipments.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchShipments.fulfilled, (state, action) => {
state.status = 'succeeded';
state.shipments = action.payload;
})
.addCase(fetchShipments.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ? action.payload.message : action.error.message;
});
},
});
export default shipmentSlice.reducer;
5. Create Payment Slice (src/redux/slices/paymentSlice.js):
javascript
Copy code
// src/redux/slices/paymentSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const PAYMENT_SERVICE_URL = process.env.REACT_APP_PAYMENT_SERVICE_URL;
const initialState = {
payments: [],
status: 'idle',
error: null,
};
export const fetchPayments = createAsyncThunk(
'payments/fetchPayments',
async (_, { getState, rejectWithValue }) => {
const token = getState().auth.token;
try {
const response = await axios.get(`${PAYMENT_SERVICE_URL}/payments`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data.payments;
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
const paymentSlice = createSlice({
name: 'payments',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPayments.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchPayments.fulfilled, (state, action) => {
state.status = 'succeeded';
state.payments = action.payload;
})
.addCase(fetchPayments.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload ? action.payload.message : action.error.message;
});
},
});
export default paymentSlice.reducer;
3.4. Routing and Navigation
1. Install React Router DOM:
If not already installed, ensure react-router-dom is in your dependencies.
bash
Copy code
npm install react-router-dom
2. Set Up Routing (src/App.js):
javascript
Copy code
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Layout from './components/Layout/Layout';
import Home from './pages/Home/Home';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Dashboard from './pages/Dashboard/Dashboard';
import Profile from './pages/Profile/Profile';
import Orders from './pages/Orders/Orders';
import Shipments from './pages/Shipments/Shipments';
import Payments from './pages/Payments/Payments';
function App() {
const token = useSelector((state) => state.auth.token);
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/login"
element={!token ? <Login /> : <Navigate to="/dashboard" />}
/>
<Route
path="/register"
element={!token ? <Register /> : <Navigate to="/dashboard" />}
/>
<Route
path="/dashboard"
element={token ? <Dashboard /> : <Navigate to="/login" />}
/>
<Route
path="/profile"
element={token ? <Profile /> : <Navigate to="/login" />}
/>
<Route
path="/orders"
element={token ? <Orders /> : <Navigate to="/login" />}
/>
<Route
path="/shipments"
element={token ? <Shipments /> : <Navigate to="/login" />}
/>
<Route
path="/payments"
element={token ? <Payments /> : <Navigate to="/login" />}
/>
{/* Add more routes as needed */}
</Routes>
</Layout>
<ToastContainer />
</Router>
);
}
export default App;
3.5. Creating Layout Components
1. Create Layout Component (src/components/Layout/Layout.js):
javascript
Copy code
// src/components/Layout/Layout.js
import React from 'react';
import { AppBar, Toolbar, Typography, Button, Box } from '@mui/material';
import { Link, useNavigate } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { logout } from '../../redux/slices/authSlice';
const Layout = ({ children }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const token = useSelector((state) => state.auth.token);
const user = useSelector((state) => state.auth.user);
const handleLogout = () => {
dispatch(logout());
navigate('/login');
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
iBranDIDit.com
</Typography>
{token ? (
<>
<Typography variant="body1" sx={{ marginRight: 2 }}>
{user.name}
</Typography>
<Button color="inherit" component={Link} to="/dashboard">
Dashboard
</Button>
<Button color="inherit" onClick={handleLogout}>
Logout
</Button>
</>
) : (
<>
<Button color="inherit" component={Link} to="/login">
Login
</Button>
<Button color="inherit" component={Link} to="/register">
Register
</Button>
</>
)}
</Toolbar>
</AppBar>
<Box sx={{ padding: 2 }}>{children}</Box>
</Box>
);
};
export default Layout;
3.6. Creating Pages and Components
We'll create several pages and components to cover the User Dashboard features.
3.6.1. Authentication Pages
1. Login Page (src/pages/Login/Login.js):
javascript
Copy code
// src/pages/Login/Login.js
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography, Container } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import { login } from '../../redux/slices/authSlice';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const authStatus = useSelector((state) => state.auth.status);
const authError = useSelector((state) => state.auth.error);
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
validationSchema: Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string().min(6, 'Password must be at least 6 characters').required('Required'),
}),
onSubmit: async (values) => {
try {
await dispatch(login(values)).unwrap();
toast.success('Logged in successfully!');
navigate('/dashboard');
} catch (err) {
toast.error(err);
}
},
});
return (
<Container maxWidth="sm">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5">
Login
</Typography>
<Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
id="email"
name="email"
label="Email Address"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<TextField
margin="normal"
fullWidth
id="password"
name="password"
label="Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={authStatus === 'loading'}
sx={{ mt: 3, mb: 2 }}
>
{authStatus === 'loading' ? 'Logging in...' : 'Login'}
</Button>
</Box>
</Box>
</Container>
);
};
export default Login;
2. Register Page (src/pages/Register/Register.js):
javascript
Copy code
// src/pages/Register/Register.js
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography, Container } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import { register } from '../../redux/slices/authSlice';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
const Register = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const authStatus = useSelector((state) => state.auth.status);
const authError = useSelector((state) => state.auth.error);
const formik = useFormik({
initialValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
validationSchema: Yup.object({
name: Yup.string().required('Required'),
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string().min(6, 'Password must be at least 6 characters').required('Required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password'), null], 'Passwords must match')
.required('Required'),
}),
onSubmit: async (values) => {
try {
await dispatch(register(values)).unwrap();
toast.success('Registered successfully!');
navigate('/dashboard');
} catch (err) {
toast.error(err);
}
},
});
return (
<Container maxWidth="sm">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5">
Register
</Typography>
<Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
id="name"
name="name"
label="Full Name"
value={formik.values.name}
onChange={formik.handleChange}
error={formik.touched.name && Boolean(formik.errors.name)}
helperText={formik.touched.name && formik.errors.name}
/>
<TextField
margin="normal"
fullWidth
id="email"
name="email"
label="Email Address"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<TextField
margin="normal"
fullWidth
id="password"
name="password"
label="Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
/>
<TextField
margin="normal"
fullWidth
id="confirmPassword"
name="confirmPassword"
label="Confirm Password"
type="password"
value={formik.values.confirmPassword}
onChange={formik.handleChange}
error={formik.touched.confirmPassword && Boolean(formik.errors.confirmPassword)}
helperText={formik.touched.confirmPassword && formik.errors.confirmPassword}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={authStatus === 'loading'}
sx={{ mt: 3, mb: 2 }}
>
{authStatus === 'loading' ? 'Registering...' : 'Register'}
</Button>
</Box>
</Box>
</Container>
);
};
export default Register;
3.6.2. Dashboard Page
1. Dashboard Page (src/pages/Dashboard/Dashboard.js):
javascript
Copy code
// src/pages/Dashboard/Dashboard.js
import React from 'react';
import { Grid, Paper, Typography, Box } from '@mui/material';
import { useSelector } from 'react-redux';
import Orders from '../../components/Dashboard/Orders';
import Shipments from '../../components/Dashboard/Shipments';
import Payments from '../../components/Dashboard/Payments';
const Dashboard = () => {
const user = useSelector((state) => state.auth.user);
return (
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
Welcome, {user.name}!
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Orders />
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Shipments />
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Payments />
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default Dashboard;
2. Orders Component (src/components/Dashboard/Orders.js):
javascript
Copy code
// src/components/Dashboard/Orders.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchOrders } from '../../redux/slices/orderSlice';
import {
Typography,
List,
ListItem,
ListItemText,
CircularProgress,
Divider,
} from '@mui/material';
const Orders = () => {
const dispatch = useDispatch();
const { orders, status, error } = useSelector((state) => state.orders);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchOrders());
}
}, [status, dispatch]);
let content;
if (status === 'loading') {
content = <CircularProgress />;
} else if (status === 'succeeded') {
content = orders.length > 0 ? (
<List>
{orders.map((order) => (
<React.Fragment key={order._id}>
<ListItem alignItems="flex-start">
<ListItemText
primary={`Order #${order._id}`}
secondary={
<>
<Typography component="span" variant="body2" color="text.primary">
Status: {order.status}
</Typography>
<br />
Total: ${order.totalAmount.toFixed(2)}
</>
}
/>
</ListItem>
<Divider component="li" />
</React.Fragment>
))}
</List>
) : (
<Typography>No orders found.</Typography>
);
} else if (status === 'failed') {
content = <Typography color="error">{error}</Typography>;
}
return (
<>
<Typography variant="h6" gutterBottom>
Order History
</Typography>
{content}
</>
);
};
export default Orders;
3. Shipments Component (src/components/Dashboard/Shipments.js):
javascript
Copy code
// src/components/Dashboard/Shipments.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchShipments } from '../../redux/slices/shipmentSlice';
import {
Typography,
List,
ListItem,
ListItemText,
CircularProgress,
Divider,
} from '@mui/material';
const Shipments = () => {
const dispatch = useDispatch();
const { shipments, status, error } = useSelector((state) => state.shipments);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchShipments());
}
}, [status, dispatch]);
let content;
if (status === 'loading') {
content = <CircularProgress />;
} else if (status === 'succeeded') {
content = shipments.length > 0 ? (
<List>
{shipments.map((shipment) => (
<React.Fragment key={shipment._id}>
<ListItem alignItems="flex-start">
<ListItemText
primary={`Shipment #${shipment._id}`}
secondary={
<>
<Typography component="span" variant="body2" color="text.primary">
Carrier: {shipment.carrier}
</Typography>
<br />
Tracking Number: {shipment.trackingNumber}
<br />
Status: {shipment.status}
</>
}
/>
</ListItem>
<Divider component="li" />
</React.Fragment>
))}
</List>
) : (
<Typography>No shipments found.</Typography>
);
} else if (status === 'failed') {
content = <Typography color="error">{error}</Typography>;
}
return (
<>
<Typography variant="h6" gutterBottom>
Shipments
</Typography>
{content}
</>
);
};
export default Shipments;
4. Payments Component (src/components/Dashboard/Payments.js):
javascript
Copy code
// src/components/Dashboard/Payments.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchPayments } from '../../redux/slices/paymentSlice';
import {
Typography,
List,
ListItem,
ListItemText,
CircularProgress,
Divider,
} from '@mui/material';
const Payments = () => {
const dispatch = useDispatch();
const { payments, status, error } = useSelector((state) => state.payments);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchPayments());
}
}, [status, dispatch]);
let content;
if (status === 'loading') {
content = <CircularProgress />;
} else if (status === 'succeeded') {
content = payments.length > 0 ? (
<List>
{payments.map((payment) => (
<React.Fragment key={payment._id}>
<ListItem alignItems="flex-start">
<ListItemText
primary={`Payment #${payment._id}`}
secondary={
<>
<Typography component="span" variant="body2" color="text.primary">
Method: {payment.paymentMethod}
</Typography>
<br />
Amount: ${payment.amount.toFixed(2)}
<br />
Status: {payment.paymentStatus}
</>
}
/>
</ListItem>
<Divider component="li" />
</React.Fragment>
))}
</List>
) : (
<Typography>No payments found.</Typography>
);
} else if (status === 'failed') {
content = <Typography color="error">{error}</Typography>;
}
return (
<>
<Typography variant="h6" gutterBottom>
Payments
</Typography>
{content}
</>
);
};
export default Payments;
3.6.3. Profile Page
1. Profile Page (src/pages/Profile/Profile.js):
javascript
Copy code
// src/pages/Profile/Profile.js
import React, { useEffect, useState } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography, Container, CircularProgress } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import axios from 'axios';
import { toast } from 'react-toastify';
import { setCredentials } from '../../redux/slices/authSlice';
const API_BASE_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const Profile = () => {
const dispatch = useDispatch();
const token = useSelector((state) => state.auth.token);
const user = useSelector((state) => state.auth.user);
const [loading, setLoading] = useState(false);
const formik = useFormik({
initialValues: {
name: user.name,
email: user.email,
password: '',
confirmPassword: '',
},
validationSchema: Yup.object({
name: Yup.string().required('Required'),
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string().min(6, 'Password must be at least 6 characters'),
confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], 'Passwords must match'),
}),
onSubmit: async (values) => {
setLoading(true);
try {
const response = await axios.put(
`${API_BASE_URL}/profile`,
{
name: values.name,
email: values.email,
password: values.password || undefined,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
dispatch(setCredentials({ user: response.data.user, token }));
toast.success('Profile updated successfully!');
} catch (err) {
toast.error(err.response?.data?.message || 'Failed to update profile');
} finally {
setLoading(false);
}
},
});
return (
<Container maxWidth="sm">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5">
Profile
</Typography>
<Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
id="name"
name="name"
label="Full Name"
value={formik.values.name}
onChange={formik.handleChange}
error={formik.touched.name && Boolean(formik.errors.name)}
helperText={formik.touched.name && formik.errors.name}
/>
<TextField
margin="normal"
fullWidth
id="email"
name="email"
label="Email Address"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<TextField
margin="normal"
fullWidth
id="password"
name="password"
label="New Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
/>
<TextField
margin="normal"
fullWidth
id="confirmPassword"
name="confirmPassword"
label="Confirm New Password"
type="password"
value={formik.values.confirmPassword}
onChange={formik.handleChange}
error={formik.touched.confirmPassword && Boolean(formik.errors.confirmPassword)}
helperText={formik.touched.confirmPassword && formik.errors.confirmPassword}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={loading}
sx={{ mt: 3, mb: 2 }}
>
{loading ? <CircularProgress size={24} /> : 'Update Profile'}
</Button>
</Box>
</Box>
</Container>
);
};
export default Profile;
3.6.4. API Service
1. API Service (src/services/api.js):
Create a centralized API service to handle all HTTP requests.
javascript
Copy code
// src/services/api.js
import axios from 'axios';
import store from '../redux/store';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const AUTH_SERVICE_URL = process.env.REACT_APP_AUTH_SERVICE_URL;
const SHIPPING_SERVICE_URL = process.env.REACT_APP_SHIPPING_SERVICE_URL;
const PAYMENT_SERVICE_URL = process.env.REACT_APP_PAYMENT_SERVICE_URL;
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add a request interceptor to include the JWT token
api.interceptors.request.use(
(config) => {
const token = store.getState().auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default api;
3.7. Creating UI Components
Leverage Material-UI components to create a consistent and responsive user interface.
3.7.1. Home Page (src/pages/Home/Home.js):
javascript
Copy code
// src/pages/Home/Home.js
import React from 'react';
import { Typography, Box } from '@mui/material';
const Home = () => {
return (
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Typography variant="h3" gutterBottom>
Welcome to iBranDIDit.com
</Typography>
<Typography variant="h6">
Your one-stop shop for all your needs. Browse products, place orders, and manage your account seamlessly.
</Typography>
</Box>
);
};
export default Home;
3.7.2. Order Details Component (Optional for Future Enhancements):
You can create components to view detailed information about each order, shipment, or payment.
3.8. Protecting Routes
Ensure that only authenticated users can access certain routes.
• PrivateRoute Component:
javascript
Copy code
// src/components/PrivateRoute/PrivateRoute.js
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
const PrivateRoute = ({ children }) => {
const token = useSelector((state) => state.auth.token);
return token ? children : <Navigate to="/login" />;
};
export default PrivateRoute;
• Usage in App.js:
javascript
Copy code
// src/App.js
// ... previous imports
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
// ... inside Routes
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
3.9. Implementing Notifications
Use React Toastify to display success and error messages to users.
• Initialize Toast Container:
Already included in App.js:
javascript
Copy code
<ToastContainer />
• Trigger Notifications:
javascript
Copy code
// Example in Login Page
toast.success('Logged in successfully!');
toast.error('Failed to login. Please check your credentials.');
3.10. Styling and Responsiveness
Material-UI ensures that components are responsive out of the box. Utilize Grid layouts and responsive design principles to ensure compatibility across devices.
________________________________________
4. Complete Code and Configuration Files
4.1. Frontend package.json
json
Copy code
{
"name": "user-dashboard",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.13.5",
"@reduxjs/toolkit": "^1.9.5",
"axios": "^1.4.0",
"formik": "^2.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.1",
"react-router-dom": "^6.14.1",
"react-scripts": "5.0.1",
"react-toastify": "^9.1.3",
"redux": "^4.2.1",
"yup": "^1.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {}
}
4.2. Redux Store Setup
1. Store Configuration (src/redux/store.js):
(Already provided above)
2. Auth Slice (src/redux/slices/authSlice.js):
(Already provided above)
3. Order Slice (src/redux/slices/orderSlice.js):
(Already provided above)
4. Shipment Slice (src/redux/slices/shipmentSlice.js):
(Already provided above)
5. Payment Slice (src/redux/slices/paymentSlice.js):
(Already provided above)
4.3. Services
1. API Service (src/services/api.js):
(Already provided above)
4.4. Components
1. Layout Component (src/components/Layout/Layout.js):
(Already provided above)
2. PrivateRoute Component (src/components/PrivateRoute/PrivateRoute.js):
(Already provided above)
3. Dashboard Components:
o Orders (src/components/Dashboard/Orders.js)
o Shipments (src/components/Dashboard/Shipments.js)
o Payments (src/components/Dashboard/Payments.js)
(All already provided above)
4.5. Pages
1. Home Page (src/pages/Home/Home.js):
(Already provided above)
2. Login Page (src/pages/Login/Login.js):
(Already provided above)
3. Register Page (src/pages/Register/Register.js):
(Already provided above)
4. Dashboard Page (src/pages/Dashboard/Dashboard.js):
(Already provided above)
5. Profile Page (src/pages/Profile/Profile.js):
(Already provided above)
4.6. Entry Point
1. index.js
javascript
Copy code
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './redux/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
4.7. Testing
1. Setup Testing Library:
Create test files alongside your components. For example, Login.test.js for the Login component.
2. Sample Test for Login Component (src/pages/Login/Login.test.js):
javascript
Copy code
// src/pages/Login/Login.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import Login from './Login';
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: null,
user: null,
status: 'idle',
error: null,
},
});
test('renders login form', () => {
render(
<Provider store={store}>
<Router>
<Login />
</Router>
</Provider>
);
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
test('validates form inputs', async () => {
render(
<Provider store={store}>
<Router>
<Login />
</Router>
</Provider>
);
fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(await screen.findAllByText(/required/i)).toHaveLength(2);
});
3. Run Tests:
bash
Copy code
npm test
4.8. Documentation
1. Create README.md
markdown
Copy code
# User Dashboard
## Overview
The **User Dashboard** is a React.js frontend application that provides users with a centralized platform to manage their profiles, view order history, track shipments, manage payments, and perform other account-related tasks. This dashboard integrates seamlessly with backend microservices, ensuring a smooth and secure user experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Application](#running-the-application)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Backend Microservices running (Auth, Order, Shipping, Payment)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Frontend Directory:**
```bash
cd frontend/user-dashboard
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-dashboard/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:5004
REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
Descriptions:
o REACT_APP_API_BASE_URL: Base URL for the Order Service.
o REACT_APP_SHIPPING_SERVICE_URL: Base URL for the Shipping Service.
o REACT_APP_PAYMENT_SERVICE_URL: Base URL for the Payment Service.
o REACT_APP_AUTH_SERVICE_URL: Base URL for the Auth Service.
Note: Replace the URLs with actual service endpoints if using Docker Compose with service names.
Running the Application
Option 1: Running Locally
3. Start Backend Microservices:
Ensure that all backend services (Auth, Order, Shipping, Payment) are running.
bash
Copy code
# Example using Docker Compose
cd path/to/ibrandidit-microservices/
docker-compose up -d
4. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
The application will run on http://localhost:3000/.
Option 2: Using Docker
5. Create Dockerfile:
dockerfile
Copy code
# frontend/user-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 3000
# Start the application
CMD [ "npm", "start" ]
6. Update Root docker-compose.yml:
Ensure that the frontend is included in the Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://localhost:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
- REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
# ... other services
volumes:
mongo-data:
7. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
Frontend will be accessible at http://localhost:3000/.
4.4. Testing the User Dashboard
1. Unit Tests:
Create test files alongside your components. For example, Dashboard.test.js for the Dashboard component.
javascript
Copy code
// src/pages/Dashboard/Dashboard.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import Dashboard from './Dashboard';
const mockStore = configureStore([]);
const store = mockStore({
auth: {
user: { name: 'John Doe' },
token: 'testtoken',
},
orders: {
orders: [],
status: 'idle',
error: null,
},
shipments: {
shipments: [],
status: 'idle',
error: null,
},
payments: {
payments: [],
status: 'idle',
error: null,
},
});
test('renders dashboard with welcome message', () => {
render(
<Provider store={store}>
<Router>
<Dashboard />
</Router>
</Provider>
);
expect(screen.getByText(/Welcome, John Doe/i)).toBeInTheDocument();
});
test('renders orders section', () => {
render(
<Provider store={store}>
<Router>
<Dashboard />
</Router>
</Provider>
);
expect(screen.getByText(/Order History/i)).toBeInTheDocument();
});
// Add more tests as needed
2. Run Tests:
bash
Copy code
npm test
4.5. Dockerization
1. Create Dockerfile
dockerfile
Copy code
# frontend/user-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve to serve the build
RUN npm install -g serve
# Expose port
EXPOSE 3000
# Start the app
CMD ["serve", "-s", "build", "-l", "3000"]
2. Update Root docker-compose.yml
(Already provided above)
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend will be accessible at http://localhost:3000/.
________________________________________
5. Documentation and Usage
5.1. Running the User Dashboard Locally
1. Ensure Backend Microservices Are Running:
Start all backend services (Auth, Order, Payment, Shipping) either locally or via Docker Compose.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
2. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
o The application will run on http://localhost:3000/.
o Login/Register to access the dashboard.
5.2. API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Auth Service: http://localhost:5003/api-docs
• Order Service: http://localhost:5004/api-docs
• Payment Service: http://localhost:5005/api-docs
• Shipping Service: http://localhost:5006/api-docs
5.3. Testing
Testing ensures that the User Dashboard behaves as expected.
1. Run Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
2. View Coverage Reports:
After tests complete, a coverage/ directory will be generated containing detailed coverage reports.
5.4. Linting and Formatting
1. Run Linting:
bash
Copy code
npm run lint
2. Run Formatting:
bash
Copy code
npm run format
5.5. Generating Documentation
1. Annotate Code with JSDoc Comments:
Ensure that all components, pages, and services are properly documented with JSDoc.
javascript
Copy code
// src/pages/Login/Login.js
/**
* Login Page Component
*
* Handles user login functionality.
*
* @returns {JSX.Element} Login form
*/
const Login = () => { /* ... */ };
2. Generate Documentation:
bash
Copy code
npm run docs
o The generated documentation will be available in the docs/ directory.
5.6. Deployment
1. Build the Application:
bash
Copy code
npm run build
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
docker build -t user-dashboard .
docker run -p 3000:3000 user-dashboard
o The application will be accessible at http://localhost:3000/.
________________________________________
6. Integration with Previous Services
Ensure that the User Dashboard interacts correctly with existing backend microservices.
6.1. Auth Service Integration
• Login/Register:
Users can log in or register, receiving a JWT token upon successful authentication.
• Protected Routes:
Dashboard and related routes require a valid JWT token.
6.2. Order Service Integration
• View Order History:
Fetch orders related to the authenticated user.
• Order Details:
View detailed information about each order, including products, total amount, and status.
6.3. Shipping Service Integration
• Track Shipments:
View shipment details and track the status of each shipment associated with orders.
6.4. Payment Service Integration
• View Payments:
View payment history, methods, and statuses.
________________________________________
7. Additional Tools and Best Practices
1. Secure Environment Variables:
o Never Commit .env Files: Ensure that .env files are excluded via .gitignore.
o Use Environment Variable Management: For production, utilize services like AWS Secrets Manager, Docker secrets, or environment variable injection provided by hosting platforms.
2. Implement HTTPS:
o Use SSL/TLS Certificates: Secure API endpoints by enforcing HTTPS, especially for handling sensitive user data.
o Configure Backend Services for HTTPS: Ensure that all backend services are set up to handle secure connections.
3. Validation and Sanitization:
o Input Validation: Use libraries like Formik and Yup for form validation.
o Sanitization: Prevent injection attacks by sanitizing inputs.
4. Error Handling:
o Consistent Error Responses: Ensure that all errors follow a consistent structure.
o Logging Sensitive Data: Avoid logging sensitive information like passwords or payment details.
5. Scalability:
o Lazy Loading: Implement code-splitting and lazy loading to optimize performance.
o Responsive Design: Ensure that the dashboard is accessible and functional across various devices and screen sizes.
6. Monitoring and Performance:
o Use Monitoring Tools: Integrate tools like Google Analytics or performance monitoring services to track user interactions and app performance.
o Optimize Performance: Regularly review and optimize the application's performance, minimizing load times and enhancing user experience.
7. API Rate Limiting:
o Protect Against Abuse: Implement rate limiting on backend services to prevent API abuse using middleware like express-rate-limit.
________________________________________
8. Troubleshooting Tips
1. Authentication Issues:
o Problem: Users are unable to log in or access protected routes.
o Solution:
Ensure that the Auth Service is running and accessible.
Verify that the JWT token is being stored correctly in local storage.
Check if the token is included in the Authorization header of API requests.
Ensure that the JWT_SECRET in both frontend and backend services matches.
2. CORS Errors:
o Problem: API calls from the frontend are blocked due to CORS policy.
o Solution:
Ensure that backend services have CORS configured to allow requests from the frontend origin.
Adjust CORS settings in backend services as needed.
3. API Endpoint Issues:
o Problem: Frontend cannot communicate with backend services.
o Solution:
Verify that the API base URLs in the .env file are correct.
Ensure that backend services are running and accessible.
Check network configurations if using Docker Compose with service names.
4. Form Validation Errors:
o Problem: Users receive unexpected validation errors.
o Solution:
Review validation schemas defined using Yup.
Ensure that form inputs adhere to validation rules.
Check for any typos or mismatches in form field names.
5. Docker Networking Problems:
o Problem: Services are not communicating correctly within Docker containers.
o Solution:
Verify Docker Compose service names and network settings.
Ensure that services depend on each other correctly using depends_on.
6. Unhandled Exceptions:
o Problem: The frontend crashes or behaves unexpectedly due to unhandled exceptions.
o Solution:
Review browser console logs for error messages.
Implement error boundaries in React to catch and handle errors gracefully.
Ensure proper error handling in API calls.
7. Responsive Design Issues:
o Problem: Dashboard layout breaks on different screen sizes.
o Solution:
Utilize Material-UI's Grid system and responsive design utilities.
Test the application on various devices and screen sizes.
Adjust component properties to ensure flexibility.
8. Performance Bottlenecks:
o Problem: The dashboard is slow to load or respond.
o Solution:
Implement lazy loading for heavy components.
Optimize API calls to reduce latency.
Use React DevTools to identify and fix performance issues.
________________________________________
9. Next Steps
With Stage 13: User Dashboard & Features completed, your eCommerce platform now offers users a comprehensive dashboard to manage their accounts effectively. This integration enhances user experience by providing essential functionalities in a centralized location.
Proceed to Stage 14: Review and Rating System
In the next stage, we'll focus on implementing a review and rating system, allowing users to provide feedback on products and services, thereby enhancing trust and engagement on your platform.
________________________________________
10. Additional Resources
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Redux Documentation: https://redux.js.org/introduction/getting-started
• Material-UI Documentation: https://mui.com/getting-started/usage/
• React Router Documentation: https://reactrouter.com/docs/en/v6/getting-started/overview
• Formik Documentation: https://formik.org/docs/overview
• Yup Documentation: https://github.com/jquense/yup
• Axios Documentation: https://axios-http.com/docs/intro
• React Toastify Documentation: https://fkhadra.github.io/react-toastify/introduction
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Shippo Node.js SDK: https://github.com/goshippo/shippo-node-client
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for the User Dashboard within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing more advanced state management solutions, integrating additional features, or enhancing security measures.
________________________________________
By following this comprehensive guide for Stage 13: User Dashboard & Features, you've successfully built a robust frontend dashboard that enhances user interaction and management within your eCommerce platform. This integration not only improves user experience but also lays the groundwork for future feature additions and scalability.
Next Steps:
Proceed to Stage 14: Review and Rating System to further enrich your platform by enabling users to provide feedback and ratings for products and services.
o1-mini
Stage 14: Social Media & Marketing Integration is a pivotal component for enhancing user engagement, expanding your platform's reach, and driving sales through strategic marketing efforts. This stage focuses on integrating social media functionalities and marketing tools into your existing eCommerce platform, enabling features like social login, social sharing, email marketing, and analytics tracking.
This comprehensive guide will walk you through setting up these features, providing all necessary code, dependencies, configuration files, and documentation to ensure easy integration, even for beginners.
________________________________________
Stage 14: Social Media & Marketing Integration
________________________________________
1. Overview
Objective:
• Social Login: Allow users to register and log in using their social media accounts (e.g., Google, Facebook).
• Social Sharing: Enable users to share products directly to their social media profiles.
• Email Marketing Integration: Connect with email marketing services (e.g., Mailchimp) to send newsletters and promotional content.
• Analytics Integration: Implement tools like Google Analytics to monitor user behavior and platform performance.
Benefits:
• Enhanced User Experience: Simplifies the registration and login process, encouraging more sign-ups.
• Increased Visibility: Social sharing amplifies product reach organically.
• Effective Marketing: Targeted email campaigns drive repeat sales and customer retention.
• Data-Driven Decisions: Analytics provide insights into user behavior, helping optimize marketing strategies.
________________________________________
2. Technology Stack
• Frontend Framework: React.js – For building the user interface.
• State Management: Redux – For managing application state.
• Routing: React Router – For handling navigation within the app.
• HTTP Client: Axios – For making API requests to backend services.
• UI Library: Material-UI (MUI) – For pre-built UI components and styling.
• Authentication: JWT (JSON Web Tokens) and Passport.js – For securing API endpoints and handling OAuth strategies.
• Form Handling: Formik and Yup – For form management and validation.
• Notifications: React Toastify – For displaying notifications to users.
• Email Marketing: Mailchimp API – For managing email campaigns.
• Analytics: Google Analytics – For tracking user interactions and platform performance.
• Social Sharing: react-share – For adding social sharing buttons.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Version Control: Git – Continuation from previous stages.
• Documentation: Markdown and Swagger – For API documentation.
________________________________________
3. Implementation Steps
3.1. Setting Up Social Login
Integrating social login enhances user experience by allowing users to register and log in using their existing social media accounts.
3.1.1. Backend: Update Auth Service
1. Install Required Dependencies:
Navigate to your Auth Service directory and install Passport.js and related strategies.
bash
Copy code
cd path/to/ibrandidit-microservices/auth-service
npm install passport passport-google-oauth20 passport-facebook express-session
2. Configure Environment Variables:
Update the .env file in the Auth Service with credentials from Google and Facebook.
env
Copy code
# Existing variables
PORT=5003
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# New variables for Social Login
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
Note: Replace the placeholders with your actual credentials obtained from Google Developers Console and Facebook for Developers.
3. Set Up Passport Strategies:
Create a new file passportConfig.js in the Auth Service to configure Passport strategies.
javascript
Copy code
// auth-service/passportConfig.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const User = require('./models/User'); // Adjust the path as needed
// Serialize user
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize user
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
// Google Strategy
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
},
async (accessToken, refreshToken, profile, done) => {
try {
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
const newUser = new User({
name: profile.displayName,
email: profile.emails[0].value,
googleId: profile.id,
});
await newUser.save();
done(null, newUser);
} catch (error) {
done(error, null);
}
}
)
);
// Facebook Strategy
passport.use(
new FacebookStrategy(
{
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: process.env.FACEBOOK_CALLBACK_URL,
profileFields: ['id', 'displayName', 'emails'],
},
async (accessToken, refreshToken, profile, done) => {
try {
const existingUser = await User.findOne({ facebookId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
const newUser = new User({
name: profile.displayName,
email: profile.emails[0].value,
facebookId: profile.id,
});
await newUser.save();
done(null, newUser);
} catch (error) {
done(error, null);
}
}
)
);
4. Integrate Passport into Express App:
Update index.js in the Auth Service to initialize Passport.
javascript
Copy code
// auth-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const passport = require('passport');
const session = require('express-session');
const cors = require('cors');
const bodyParser = require('body-parser');
const authRoutes = require('./routes/authRoutes'); // Adjust the path as needed
const passportConfig = require('./passportConfig'); // Import Passport config
const winston = require('winston');
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
// Middleware
app.use(cors({
origin: 'http://localhost:3000', // Frontend URL
credentials: true,
}));
app.use(bodyParser.json());
// Express session
app.use(
session({
secret: process.env.JWT_SECRET,
resave: false,
saveUninitialized: true,
})
);
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Connect to MongoDB
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => winston.info('Auth Service connected to MongoDB'))
.catch((err) => {
winston.error('Auth Service failed to connect to MongoDB:', err);
process.exit(1);
});
// Routes
app.use('/auth', authRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Welcome to Auth Service API');
});
// Start Server
const PORT = process.env.PORT || 5003;
app.listen(PORT, () => {
winston.info(`Auth Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
5. Create User Model:
Update your User model to include fields for social IDs.
javascript
Copy code
// auth-service/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
},
googleId: {
type: String,
},
facebookId: {
type: String,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
UserSchema.pre('save', function (next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('User', UserSchema);
6. Create Authentication Routes:
Update your Auth Routes to handle social authentication.
javascript
Copy code
// auth-service/routes/authRoutes.js
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
// Local Authentication Routes
// ... Existing routes for registration and login
// Google OAuth
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication, generate JWT and redirect
const payload = {
id: req.user.id,
name: req.user.name,
email: req.user.email,
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
// Redirect to frontend with token
res.redirect(`http://localhost:3000?token=${token}`);
}
);
// Facebook OAuth
router.get('/facebook', passport.authenticate('facebook', { scope: ['email'] }));
router.get(
'/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication, generate JWT and redirect
const payload = {
id: req.user.id,
name: req.user.name,
email: req.user.email,
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
// Redirect to frontend with token
res.redirect(`http://localhost:3000?token=${token}`);
}
);
module.exports = router;
Explanation:
o Google OAuth:
GET /auth/google: Initiates Google OAuth flow.
GET /auth/google/callback: Handles callback from Google, authenticates the user, generates a JWT, and redirects to the frontend with the token.
o Facebook OAuth:
GET /auth/facebook: Initiates Facebook OAuth flow.
GET /auth/facebook/callback: Handles callback from Facebook, authenticates the user, generates a JWT, and redirects to the frontend with the token.
7. Update Frontend: Integrate Social Login
1. Update Environment Variables:
Add the Auth Service URL to the frontend .env file if not already present.
env
Copy code
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
2. Update Login Page to Include Social Login Buttons:
javascript
Copy code
// frontend/user-dashboard/src/pages/Login/Login.js
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography, Container, Divider } from '@mui/material';
import { useDispatch, useSelector } from 'react-redux';
import { login } from '../../redux/slices/authSlice';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Google as GoogleIcon, Facebook as FacebookIcon } from '@mui/icons-material';
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const authStatus = useSelector((state) => state.auth.status);
const authError = useSelector((state) => state.auth.error);
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
validationSchema: Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
password: Yup.string().min(6, 'Password must be at least 6 characters').required('Required'),
}),
onSubmit: async (values) => {
try {
await dispatch(login(values)).unwrap();
toast.success('Logged in successfully!');
navigate('/dashboard');
} catch (err) {
toast.error(err);
}
},
});
return (
<Container maxWidth="sm">
<Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography component="h1" variant="h5">
Login
</Typography>
<Box component="form" onSubmit={formik.handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
fullWidth
id="email"
name="email"
label="Email Address"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<TextField
margin="normal"
fullWidth
id="password"
name="password"
label="Password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={authStatus === 'loading'}
sx={{ mt: 3, mb: 2 }}
>
{authStatus === 'loading' ? 'Logging in...' : 'Login'}
</Button>
</Box>
<Divider sx={{ width: '100%', mt: 2, mb: 2 }}>OR</Divider>
<Button
variant="outlined"
color="primary"
startIcon={<GoogleIcon />}
href={`${process.env.REACT_APP_AUTH_SERVICE_URL}/auth/google`}
sx={{ mb: 1, width: '100%' }}
>
Sign in with Google
</Button>
<Button
variant="outlined"
color="primary"
startIcon={<FacebookIcon />}
href={`${process.env.REACT_APP_AUTH_SERVICE_URL}/auth/facebook`}
sx={{ mb: 1, width: '100%' }}
>
Sign in with Facebook
</Button>
</Box>
</Container>
);
};
export default Login;
Explanation:
Added Google and Facebook login buttons using Material-UI buttons with icons.
Clicking these buttons redirects users to the respective OAuth flows handled by the backend Auth Service.
After successful authentication, users are redirected back to the frontend with a JWT token.
8. Handle JWT Token from OAuth Redirect:
Update the frontend to capture the JWT token from the URL after successful social login.
1. Create a Component to Handle Redirect (src/pages/AuthCallback/AuthCallback.js):
javascript
Copy code
// src/pages/AuthCallback/AuthCallback.js
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { setCredentials } from '../../redux/slices/authSlice';
import { toast } from 'react-toastify';
const AuthCallback = () => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
const params = new URLSearchParams(location.search);
const token = params.get('token');
if (token) {
// Decode token to get user info
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
const user = JSON.parse(jsonPayload);
// Dispatch to Redux store
dispatch(setCredentials({ user, token }));
toast.success('Logged in successfully!');
navigate('/dashboard');
} else {
toast.error('Authentication failed. Please try again.');
navigate('/login');
}
}, [location, dispatch, navigate]);
return null;
};
export default AuthCallback;
2. Add Route for Auth Callback (src/App.js):
javascript
Copy code
// src/App.js
// ... previous imports
import AuthCallback from './pages/AuthCallback/AuthCallback';
function App() {
// ... previous code
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/login"
element={!token ? <Login /> : <Navigate to="/dashboard" />}
/>
<Route
path="/register"
element={!token ? <Register /> : <Navigate to="/dashboard" />}
/>
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/orders"
element={
<PrivateRoute>
<Orders />
</PrivateRoute>
}
/>
<Route
path="/shipments"
element={
<PrivateRoute>
<Shipments />
</PrivateRoute>
}
/>
<Route
path="/payments"
element={
<PrivateRoute>
<Payments />
</PrivateRoute>
}
/>
<Route path="/auth/callback" element={<AuthCallback />} />
{/* Add more routes as needed */}
</Routes>
</Layout>
<ToastContainer />
</Router>
);
}
export default App;
Explanation:
After successful social authentication, users are redirected to /auth/callback with a JWT token in the URL.
The AuthCallback component extracts the token, decodes it to get user information, updates the Redux store, and navigates the user to the dashboard.
If authentication fails, users are redirected back to the login page with an error message.
3.1.2. Frontend: Update Redux Auth Slice to Handle Social Login
Ensure that your authSlice.js can handle setting credentials when receiving a token from social login.
javascript
Copy code
// frontend/user-dashboard/src/redux/slices/authSlice.js
// ... existing imports and code
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout(state) {
state.user = null;
state.token = null;
localStorage.removeItem('token');
},
setCredentials(state, action) {
const { user, token } = action.payload;
state.user = user;
state.token = token;
localStorage.setItem('token', token);
},
},
// ... existing extraReducers
});
export const { logout, setCredentials } = authSlice.actions;
export default authSlice.reducer;
3.2. Implement Social Sharing
Allow users to share products directly to their social media profiles, enhancing product visibility and attracting potential customers.
3.2.1. Install react-share Dependency
Navigate to your frontend project directory and install react-share.
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install react-share
3.2.2. Add Social Sharing Buttons to Product Pages
1. Create a Social Share Component (src/components/SocialShare/SocialShare.js):
javascript
Copy code
// src/components/SocialShare/SocialShare.js
import React from 'react';
import {
FacebookShareButton,
TwitterShareButton,
LinkedinShareButton,
WhatsappShareButton,
FacebookIcon,
TwitterIcon,
LinkedinIcon,
WhatsappIcon,
} from 'react-share';
import { Box, Typography } from '@mui/material';
const SocialShare = ({ url, title }) => {
return (
<Box sx={{ display: 'flex', alignItems: 'center', mt: 2 }}>
<Typography variant="subtitle1" sx={{ mr: 2 }}>
Share:
</Typography>
<FacebookShareButton url={url} quote={title} style={{ marginRight: '8px' }}>
<FacebookIcon size={32} round />
</FacebookShareButton>
<TwitterShareButton url={url} title={title} style={{ marginRight: '8px' }}>
<TwitterIcon size={32} round />
</TwitterShareButton>
<LinkedinShareButton url={url} title={title} style={{ marginRight: '8px' }}>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
<WhatsappShareButton url={url} title={title} separator=":: " style={{ marginRight: '8px' }}>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
</Box>
);
};
export default SocialShare;
2. Integrate SocialShare Component into Product Page:
Assuming you have a ProductDetail page/component.
javascript
Copy code
// src/pages/ProductDetail/ProductDetail.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
Typography,
Box,
CircularProgress,
Button,
Grid,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import SocialShare from '../../components/SocialShare/SocialShare';
import { toast } from 'react-toastify';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const ProductDetail = () => {
const { id } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const token = useSelector((state) => state.auth.token);
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/products/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setProduct(response.data.product);
} catch (error) {
toast.error('Failed to fetch product details');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [id, token]);
if (loading) {
return <CircularProgress />;
}
if (!product) {
return <Typography>Product not found.</Typography>;
}
const productUrl = window.location.href;
const productTitle = product.name;
return (
<Box sx={{ mt: 4 }}>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<img src={product.imageUrl} alt={product.name} style={{ width: '100%' }} />
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h4" gutterBottom>
{product.name}
</Typography>
<Typography variant="h6" color="text.secondary" gutterBottom>
${product.price.toFixed(2)}
</Typography>
<Typography variant="body1" gutterBottom>
{product.description}
</Typography>
<Button variant="contained" color="primary">
Add to Cart
</Button>
<SocialShare url={productUrl} title={productTitle} />
</Grid>
</Grid>
</Box>
);
};
export default ProductDetail;
Explanation:
o The SocialShare component is integrated into the ProductDetail page.
o Users can share the product on Facebook, Twitter, LinkedIn, and WhatsApp.
o The component receives the current URL and product title as props.
3.3. Integrate Email Marketing (Mailchimp)
Email marketing is essential for engaging with your customers, sending newsletters, promotional offers, and driving repeat sales.
3.3.1. Backend: Update Auth Service
1. Install Mailchimp Marketing SDK:
Navigate to your Auth Service directory and install the Mailchimp SDK.
bash
Copy code
cd path/to/ibrandidit-microservices/auth-service
npm install @mailchimp/mailchimp_marketing
2. Configure Environment Variables:
Update the .env file in the Auth Service with Mailchimp credentials.
env
Copy code
# Existing variables
PORT=5003
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
# Mailchimp Configuration
MAILCHIMP_API_KEY=your_mailchimp_api_key
MAILCHIMP_SERVER_PREFIX=your_server_prefix # e.g., us19
MAILCHIMP_LIST_ID=your_mailchimp_list_id
Note:
o MAILCHIMP_API_KEY: Obtain from your Mailchimp account.
o MAILCHIMP_SERVER_PREFIX: The server prefix is the part after the hyphen in your API key (e.g., if your API key ends with us19, then MAILCHIMP_SERVER_PREFIX=us19).
o MAILCHIMP_LIST_ID: The ID of the Mailchimp audience/list you want to add subscribers to.
3. Configure Mailchimp in Auth Service:
Create a new file mailchimp.js in the Auth Service.
javascript
Copy code
// auth-service/mailchimp.js
const mailchimp = require('@mailchimp/mailchimp_marketing');
mailchimp.setConfig({
apiKey: process.env.MAILCHIMP_API_KEY,
server: process.env.MAILCHIMP_SERVER_PREFIX,
});
const subscribeUser = async (email, firstName, lastName) => {
try {
const response = await mailchimp.lists.addListMember(process.env.MAILCHIMP_LIST_ID, {
email_address: email,
status: 'subscribed',
merge_fields: {
FNAME: firstName,
LNAME: lastName,
},
});
return response;
} catch (error) {
console.error('Error subscribing user to Mailchimp:', error);
throw error;
}
};
module.exports = {
subscribeUser,
};
4. Update Registration and Social Login to Subscribe Users:
Modify authRoutes.js to subscribe users to Mailchimp upon registration and social login.
javascript
Copy code
// auth-service/routes/authRoutes.js
const express = require('express');
const passport = require('passport');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const { subscribeUser } = require('../mailchimp');
const router = express.Router();
// Registration Route
router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
try {
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
const newUser = new User({ name, email, password }); // Ensure password is hashed
await newUser.save();
// Subscribe to Mailchimp
const [firstName, lastName] = name.split(' ');
await subscribeUser(email, firstName, lastName);
// Generate JWT
const payload = { id: newUser.id, name: newUser.name, email: newUser.email };
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
res.status(201).json({ user: payload, token });
} catch (error) {
res.status(500).json({ message: 'Server error' });
}
});
// Social Login Routes
// ... existing Google and Facebook routes
router.get(
'/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
async (req, res) => {
try {
const [firstName, lastName] = req.user.name.split(' ');
await subscribeUser(req.user.email, firstName, lastName);
} catch (error) {
console.error('Mailchimp subscription error:', error);
}
const payload = {
id: req.user.id,
name: req.user.name,
email: req.user.email,
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
res.redirect(`http://localhost:3000?token=${token}`);
}
);
router.get(
'/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/login' }),
async (req, res) => {
try {
const [firstName, lastName] = req.user.name.split(' ');
await subscribeUser(req.user.email, firstName, lastName);
} catch (error) {
console.error('Mailchimp subscription error:', error);
}
const payload = {
id: req.user.id,
name: req.user.name,
email: req.user.email,
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });
res.redirect(`http://localhost:3000?token=${token}`);
}
);
module.exports = router;
Explanation:
o Upon successful registration or social login, the user is subscribed to the Mailchimp audience/list.
o The subscribeUser function handles adding the user to Mailchimp.
o Errors during subscription are logged but do not prevent the authentication process.
3.3.2. Frontend: Add Newsletter Subscription
Allow users to subscribe to newsletters directly from the frontend.
1. Create Newsletter Subscription Component (src/components/Newsletter/Newsletter.js):
javascript
Copy code
// src/components/Newsletter/Newsletter.js
import React from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography } from '@mui/material';
import axios from 'axios';
import { toast } from 'react-toastify';
const AuthServiceURL = process.env.REACT_APP_AUTH_SERVICE_URL;
const Newsletter = () => {
const formik = useFormik({
initialValues: {
email: '',
},
validationSchema: Yup.object({
email: Yup.string().email('Invalid email address').required('Required'),
}),
onSubmit: async (values, { resetForm }) => {
try {
const response = await axios.post(`${AuthServiceURL}/subscribe`, { email: values.email });
toast.success('Subscribed to newsletter successfully!');
resetForm();
} catch (error) {
toast.error('Failed to subscribe to newsletter.');
}
},
});
return (
<Box sx={{ mt: 4, p: 2, border: '1px solid #ccc', borderRadius: '8px' }}>
<Typography variant="h6" gutterBottom>
Subscribe to our Newsletter
</Typography>
<form onSubmit={formik.handleSubmit}>
<TextField
fullWidth
id="email"
name="email"
label="Email Address"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<Button
type="submit"
variant="contained"
color="primary"
sx={{ mt: 2 }}
disabled={formik.isSubmitting}
>
Subscribe
</Button>
</form>
</Box>
);
};
export default Newsletter;
2. Add Newsletter Component to Home Page (src/pages/Home/Home.js):
javascript
Copy code
// src/pages/Home/Home.js
import React from 'react';
import { Typography, Box } from '@mui/material';
import Newsletter from '../../components/Newsletter/Newsletter';
const Home = () => {
return (
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Typography variant="h3" gutterBottom>
Welcome to iBranDIDit.com
</Typography>
<Typography variant="h6">
Your one-stop shop for all your needs. Browse products, place orders, and manage your account seamlessly.
</Typography>
<Newsletter />
</Box>
);
};
export default Home;
Explanation:
o The Newsletter component allows users to subscribe to your newsletter.
o Upon successful subscription, users receive a success notification.
o Errors during subscription trigger an error notification.
3. Update Backend Auth Service to Handle Newsletter Subscriptions:
1. Update authRoutes.js to Add Subscription Route:
javascript
Copy code
// auth-service/routes/authRoutes.js
// ... existing imports and routes
const { subscribeUser } = require('../mailchimp');
// Newsletter Subscription Route
router.post('/subscribe', async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: 'Email is required' });
}
try {
const [firstName, lastName] = (req.body.name || 'Subscriber').split(' ');
await subscribeUser(email, firstName, lastName);
res.status(200).json({ message: 'Subscribed successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to subscribe' });
}
});
module.exports = router;
Note: Since users may not provide their name when subscribing via the newsletter form, a default name ('Subscriber') is used.
3.4. Integrate Analytics (Google Analytics)
Implementing analytics helps you understand user behavior, track platform performance, and make data-driven decisions.
3.4.1. Set Up Google Analytics
1. Create a Google Analytics Account:
o Go to Google Analytics and sign in with your Google account.
o Set up a new property for your eCommerce platform to obtain a Tracking ID (e.g., G-XXXXXXXXXX).
2. Install Google Analytics in Frontend:
1. Install react-ga4:
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install react-ga4
2. Configure Google Analytics (src/services/analytics.js):
javascript
Copy code
// src/services/analytics.js
import ReactGA from 'react-ga4';
const TRACKING_ID = process.env.REACT_APP_GOOGLE_ANALYTICS_ID; // e.g., G-XXXXXXXXXX
export const initGA = () => {
ReactGA.initialize(TRACKING_ID);
};
export const logPageView = () => {
ReactGA.send({ hitType: 'pageview', page: window.location.pathname + window.location.search });
};
3. Initialize Analytics in index.js:
javascript
Copy code
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './redux/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { initGA, logPageView } from './services/analytics';
// Load environment variables
// Assuming you have a .env file with REACT_APP_GOOGLE_ANALYTICS_ID
initGA();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// Log initial page view
logPageView();
// Listen for route changes
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const RouteChangeTracker = () => {
const location = useLocation();
useEffect(() => {
logPageView();
}, [location]);
return null;
};
// Update App.js to include RouteChangeTracker
// src/App.js
import RouteChangeTracker from './components/RouteChangeTracker';
function App() {
// ... existing code
return (
<Router>
<Layout>
<RouteChangeTracker />
<Routes>
{/* existing routes */}
</Routes>
</Layout>
<ToastContainer />
</Router>
);
}
export default App;
4. Update Environment Variables:
Add your Google Analytics Tracking ID to the frontend .env file.
env
Copy code
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
5. Create RouteChangeTracker Component (src/components/RouteChangeTracker.js):
javascript
Copy code
// src/components/RouteChangeTracker.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { logPageView } from '../services/analytics';
const RouteChangeTracker = () => {
const location = useLocation();
useEffect(() => {
logPageView();
}, [location]);
return null;
};
export default RouteChangeTracker;
Explanation:
The RouteChangeTracker component listens for route changes and logs page views to Google Analytics.
It ensures that every page navigation is tracked accurately.
3.5. Configure SEO Optimization
While not explicitly mentioned, integrating SEO best practices complements social media and marketing efforts.
3.5.1. Install react-helmet
react-helmet allows you to manage the document head for better SEO.
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install react-helmet
3.5.2. Implement react-helmet in Pages
1. Update Home Page (src/pages/Home/Home.js):
javascript
Copy code
// src/pages/Home/Home.js
import React from 'react';
import { Typography, Box } from '@mui/material';
import Newsletter from '../../components/Newsletter/Newsletter';
import { Helmet } from 'react-helmet';
const Home = () => {
return (
<Box sx={{ textAlign: 'center', mt: 4 }}>
<Helmet>
<title>iBranDIDit.com - Your One-Stop Shop</title>
<meta name="description" content="Browse and purchase a wide variety of products at iBranDIDit.com. Quality products, fast shipping, and excellent customer service." />
</Helmet>
<Typography variant="h3" gutterBottom>
Welcome to iBranDIDit.com
</Typography>
<Typography variant="h6">
Your one-stop shop for all your needs. Browse products, place orders, and manage your account seamlessly.
</Typography>
<Newsletter />
</Box>
);
};
export default Home;
2. Update Product Detail Page (src/pages/ProductDetail/ProductDetail.js):
javascript
Copy code
// src/pages/ProductDetail/ProductDetail.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
Typography,
Box,
CircularProgress,
Button,
Grid,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import SocialShare from '../../components/SocialShare/SocialShare';
import { toast } from 'react-toastify';
import { Helmet } from 'react-helmet';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const ProductDetail = () => {
const { id } = useParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const token = useSelector((state) => state.auth.token);
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/products/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
setProduct(response.data.product);
} catch (error) {
toast.error('Failed to fetch product details');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [id, token]);
if (loading) {
return <CircularProgress />;
}
if (!product) {
return <Typography>Product not found.</Typography>;
}
const productUrl = window.location.href;
const productTitle = product.name;
return (
<Box sx={{ mt: 4 }}>
<Helmet>
<title>{product.name} - iBranDIDit.com</title>
<meta name="description" content={product.description} />
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.imageUrl} />
<meta property="og:url" content={productUrl} />
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
<Grid container spacing={4}>
<Grid item xs={12} md={6}>
<img src={product.imageUrl} alt={product.name} style={{ width: '100%' }} />
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h4" gutterBottom>
{product.name}
</Typography>
<Typography variant="h6" color="text.secondary" gutterBottom>
${product.price.toFixed(2)}
</Typography>
<Typography variant="body1" gutterBottom>
{product.description}
</Typography>
<Button variant="contained" color="primary">
Add to Cart
</Button>
<SocialShare url={productUrl} title={productTitle} />
</Grid>
</Grid>
</Box>
);
};
export default ProductDetail;
Explanation:
o The Helmet component manages the document head, setting the page title and meta tags.
o Open Graph (og:) and Twitter meta tags enhance link previews when shared on social media.
o Ensures that shared links display correctly on platforms like Facebook and Twitter.
3.6. Testing
Ensure that all new features work as expected.
3.6.1. Backend Testing
1. Test Social Login Routes:
o Verify that /auth/google and /auth/facebook routes initiate OAuth flows.
o Ensure that upon successful authentication, users are redirected with a valid JWT token.
2. Test Mailchimp Subscription:
o Register a new user and confirm that their email is added to the Mailchimp list.
o Test error handling by providing invalid email addresses.
3.6.2. Frontend Testing
1. Test Social Login Buttons:
o Click on "Sign in with Google" and "Sign in with Facebook" buttons.
o Ensure that users are redirected to the respective OAuth consent screens.
o Confirm that upon successful authentication, users are redirected back with access to the dashboard.
2. Test Newsletter Subscription:
o Enter a valid email address and subscribe.
o Confirm that the email is added to the Mailchimp list.
o Test with invalid emails to ensure validation works.
3. Test Social Sharing Buttons:
o Share a product on various social media platforms.
o Verify that the shared link displays correctly with appropriate images and descriptions.
4. Test Analytics Integration:
o Navigate through different pages and verify that page views are logged in Google Analytics.
3.6.3. Sample Test for Newsletter Component (src/components/Newsletter/Newsletter.test.js):
javascript
Copy code
// src/components/Newsletter/Newsletter.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Newsletter from './Newsletter';
import axios from 'axios';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
describe('Newsletter Subscription', () => {
test('renders subscription form', () => {
render(
<>
<Newsletter />
<ToastContainer />
</>
);
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /subscribe/i })).toBeInTheDocument();
});
test('submits form with valid email', async () => {
axios.post.mockResolvedValue({ data: { message: 'Subscribed successfully' } });
render(
<>
<Newsletter />
<ToastContainer />
</>
);
fireEvent.change(screen.getByLabelText(/email address/i), {
target: { value: 'test@example.com' },
});
fireEvent.click(screen.getByRole('button', { name: /subscribe/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalledWith(
`${process.env.REACT_APP_AUTH_SERVICE_URL}/subscribe`,
{ email: 'test@example.com' }
);
expect(screen.getByText(/subscribed to newsletter successfully/i)).toBeInTheDocument();
});
});
test('shows error on failed subscription', async () => {
axios.post.mockRejectedValue(new Error('Failed to subscribe'));
render(
<>
<Newsletter />
<ToastContainer />
</>
);
fireEvent.change(screen.getByLabelText(/email address/i), {
target: { value: 'invalid-email' },
});
fireEvent.click(screen.getByRole('button', { name: /subscribe/i }));
await waitFor(() => {
expect(screen.getByText(/failed to subscribe to newsletter/i)).toBeInTheDocument();
});
});
});
3.7. Documentation
1. Create README.md
Update the README.md in the frontend/user-dashboard directory to include information about the new features.
markdown
Copy code
# User Dashboard
## Overview
The **User Dashboard** is a React.js frontend application that provides users with a centralized platform to manage their profiles, view order history, track shipments, manage payments, subscribe to newsletters, and interact with social media. This dashboard integrates seamlessly with backend microservices, ensuring a smooth and secure user experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Application](#running-the-application)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Backend Microservices running (Auth, Order, Payment, Shipping)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Frontend Directory:**
```bash
cd frontend/user-dashboard
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-dashboard/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:5004
REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
Descriptions:
o REACT_APP_API_BASE_URL: Base URL for the Order Service.
o REACT_APP_SHIPPING_SERVICE_URL: Base URL for the Shipping Service.
o REACT_APP_PAYMENT_SERVICE_URL: Base URL for the Payment Service.
o REACT_APP_AUTH_SERVICE_URL: Base URL for the Auth Service.
o REACT_APP_GOOGLE_ANALYTICS_ID: Your Google Analytics Tracking ID.
Note: Replace the URLs with actual service endpoints if using Docker Compose with service names.
Running the Application
Option 1: Running Locally
3. Start Backend Microservices:
Ensure that all backend services (Auth, Order, Payment, Shipping) are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
4. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
The application will run on http://localhost:3000/.
Login/Register to access the dashboard.
Option 2: Using Docker
5. Create Dockerfile:
dockerfile
Copy code
# frontend/user-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve to serve the build
RUN npm install -g serve
# Expose port
EXPOSE 3000
# Start the app
CMD ["serve", "-s", "build", "-l", "3000"]
6. Update Root docker-compose.yml:
Ensure that the frontend is included in the Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://localhost:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
- REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
# ... other services
volumes:
mongo-data:
Explanation:
The Auth Service is updated with new environment variables for Google, Facebook, and Mailchimp integrations.
The Frontend service is configured to include the Google Analytics ID.
7. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
Frontend will be accessible at http://localhost:3000/.
Auth Service at http://localhost:5003/.
Order Service at http://localhost:5004/.
Payment Service at http://localhost:5005/.
Shipping Service at http://localhost:5006/.
MongoDB and RabbitMQ will be running as per the Docker Compose configuration.
3.8. Finalizing and Deployment
After completing all integrations, ensure that your application is ready for deployment.
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
The Docker Compose setup allows you to deploy the entire application stack seamlessly.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Ensure that all environment variables are correctly set in the .env files.
o Replace placeholder values with actual credentials before deploying to production.
________________________________________
4. Complete Code and Configuration Files
4.1. Backend: Auth Service
1. auth-service/package.json
json
Copy code
{
"name": "auth-service",
"version": "1.0.0",
"description": "Authentication Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"@mailchimp/mailchimp_marketing": "^3.0.76",
"amqplib": "^0.10.3",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"passport": "^0.6.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth20": "^2.0.0",
"winston": "^3.8.2"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8"
}
}
2. auth-service/index.js
(Already provided above)
3. auth-service/models/User.js
(Already provided above)
4. auth-service/passportConfig.js
(Already provided above)
5. auth-service/routes/authRoutes.js
(Already provided above)
6. auth-service/mailchimp.js
(Already provided above)
7. auth-service/.env.example
env
Copy code
PORT=5003
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
# Facebook OAuth
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
# Mailchimp
MAILCHIMP_API_KEY=your_mailchimp_api_key
MAILCHIMP_SERVER_PREFIX=your_server_prefix
MAILCHIMP_LIST_ID=your_mailchimp_list_id
4.2. Frontend: User Dashboard
1. frontend/user-dashboard/package.json
(Already provided above)
2. frontend/user-dashboard/src/redux/slices/authSlice.js
(Already provided above)
3. frontend/user-dashboard/src/redux/slices/orderSlice.js
(Already provided above)
4. frontend/user-dashboard/src/redux/slices/shipmentSlice.js
(Already provided above)
5. frontend/user-dashboard/src/redux/slices/paymentSlice.js
(Already provided above)
6. frontend/user-dashboard/src/redux/store.js
(Already provided above)
7. frontend/user-dashboard/src/services/api.js
(Already provided above)
8. frontend/user-dashboard/src/services/analytics.js
(Already provided above)
9. frontend/user-dashboard/src/components/Layout/Layout.js
(Already provided above)
10. frontend/user-dashboard/src/components/PrivateRoute/PrivateRoute.js
(Already provided above)
11. frontend/user-dashboard/src/components/Dashboard/Orders.js
(Already provided above)
12. frontend/user-dashboard/src/components/Dashboard/Shipments.js
(Already provided above)
13. frontend/user-dashboard/src/components/Dashboard/Payments.js
(Already provided above)
14. frontend/user-dashboard/src/components/SocialShare/SocialShare.js
(Already provided above)
15. frontend/user-dashboard/src/components/Newsletter/Newsletter.js
(Already provided above)
16. frontend/user-dashboard/src/pages/AuthCallback/AuthCallback.js
(Already provided above)
17. frontend/user-dashboard/src/pages/Home/Home.js
(Already provided above)
18. frontend/user-dashboard/src/pages/Login/Login.js
(Already provided above)
19. frontend/user-dashboard/src/pages/Register/Register.js
(Already provided above)
20. frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided above)
21. frontend/user-dashboard/src/pages/Profile/Profile.js
(Already provided above)
22. frontend/user-dashboard/src/pages/ProductDetail/ProductDetail.js
(Already provided above)
23. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided above)
24. frontend/user-dashboard/src/App.js
(Already provided above)
25. frontend/user-dashboard/src/index.js
(Already provided above)
26. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided above)
27. frontend/user-dashboard/src/pages/AuthCallback/AuthCallback.js
(Already provided above)
4.3. Docker Configuration
1. ibrandidit-microservices/docker-compose.yml
(Already provided above)
2. frontend/user-dashboard/Dockerfile
(Already provided above)
________________________________________
5. Documentation and Usage
5.1. Running the Application Locally
1. Start Backend Microservices:
Ensure all backend services (Auth, Order, Payment, Shipping) are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
2. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
o The application will run on http://localhost:3000/.
o Login/Register to access the dashboard.
5.2. API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Auth Service: http://localhost:5003/api-docs
• Order Service: http://localhost:5004/api-docs
• Payment Service: http://localhost:5005/api-docs
• Shipping Service: http://localhost:5006/api-docs
5.3. Testing
Testing ensures that the Social Media & Marketing integrations work as expected.
1. Run Backend Tests:
bash
Copy code
cd path/to/ibrandidit-microservices/auth-service
npm test
2. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Ensure all tests pass, especially those related to social login and newsletter subscriptions.
5.4. Linting and Formatting
Maintain code quality using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd frontend/user-dashboard
npm run lint
2. Run Formatting:
bash
Copy code
cd frontend/user-dashboard
npm run format
5.5. Deployment
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend will be accessible at http://localhost:3000/.
o Ensure that all environment variables are correctly set in the .env files.
o Replace placeholder values with actual credentials before deploying to production.
5.6. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/social-login
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat(auth-service): add Google and Facebook OAuth strategies"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/social-login
o Submit a pull request from your fork to the main repository for review.
________________________________________
6. Integration with Previous Services
Ensure that the Social Media & Marketing Integration seamlessly interacts with existing microservices and frontend components.
6.1. Auth Service Integration
• Social Login: Users can register and log in using Google or Facebook accounts.
• Newsletter Subscription: Users subscribing via the frontend are added to the Mailchimp list through the Auth Service.
6.2. Order, Shipping, and Payment Services Integration
• Authenticated Requests: Ensure that all API requests from the frontend include the JWT token for authorization.
• User Data Synchronization: Social login provides user data (name, email) that can be utilized across services like orders and payments.
6.3. Frontend Dashboard Integration
• User Information: Display user information obtained from social login.
• Order and Shipment Data: Fetch and display order history and shipment tracking.
• Payments: Manage payment methods and view payment history.
________________________________________
7. Troubleshooting Tips
1. Social Login Issues:
o Problem: Users are unable to log in using Google or Facebook.
o Solution:
Ensure that OAuth credentials (Client ID and Secret) are correctly set in the Auth Service .env file.
Verify that the callback URLs match those configured in the Google and Facebook developer consoles.
Check backend logs for any authentication errors.
2. Mailchimp Subscription Failures:
o Problem: Users are not being subscribed to the Mailchimp list.
o Solution:
Confirm that Mailchimp API credentials (MAILCHIMP_API_KEY, MAILCHIMP_SERVER_PREFIX, MAILCHIMP_LIST_ID) are correctly set.
Ensure that the Mailchimp list ID exists and is active.
Check backend logs for any errors related to Mailchimp API calls.
3. CORS Errors:
o Problem: Frontend API requests are blocked due to CORS policy.
o Solution:
Ensure that the backend services have CORS configured to allow requests from the frontend origin (http://localhost:3000).
Verify that the cors middleware in the backend is correctly set up.
4. JWT Token Handling:
o Problem: Frontend is unable to access protected routes despite successful login.
o Solution:
Confirm that the JWT token is stored correctly in local storage.
Ensure that the token is included in the Authorization header of API requests.
Verify that the backend services are correctly verifying the JWT token.
5. Google Analytics Not Tracking:
o Problem: Page views and user interactions are not being recorded in Google Analytics.
o Solution:
Ensure that the correct Tracking ID is set in the frontend .env file.
Verify that the react-ga4 is correctly initialized and integrated.
Check the Network tab in the browser's developer tools to see if Analytics requests are being made.
6. Social Sharing Not Working Properly:
o Problem: Shared links do not display correctly on social media platforms.
o Solution:
Ensure that the Open Graph (og:) and Twitter meta tags are correctly set using react-helmet.
Verify that the product pages have unique and descriptive meta tags.
Use tools like Facebook Sharing Debugger to inspect and debug shared links.
7. Docker Networking Issues:
o Problem: Services are unable to communicate within Docker containers.
o Solution:
Verify that all services are correctly defined in the docker-compose.yml.
Ensure that environment variables use service names instead of localhost if services are networked via Docker.
Check Docker logs for any connectivity issues.
8. Unhandled Exceptions:
o Problem: Application crashes or behaves unexpectedly due to unhandled errors.
o Solution:
Implement comprehensive error handling in both frontend and backend.
Use try-catch blocks around asynchronous operations.
Review logs to identify and fix errors.
________________________________________
8. Additional Resources
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Redux Documentation: https://redux.js.org/introduction/getting-started
• Material-UI Documentation: https://mui.com/getting-started/usage/
• React Router Documentation: https://reactrouter.com/docs/en/v6/getting-started/overview
• Formik Documentation: https://formik.org/docs/overview
• Yup Documentation: https://github.com/jquense/yup
• Axios Documentation: https://axios-http.com/docs/intro
• React Toastify Documentation: https://fkhadra.github.io/react-toastify/introduction
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Passport.js Documentation: http://www.passportjs.org/docs/
• Mailchimp Marketing API Documentation: https://mailchimp.com/developer/marketing/api/
• Google Analytics Documentation: https://developers.google.com/analytics
• react-share Documentation: https://github.com/nygardk/react-share
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 14: Social Media & Marketing Integration within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as handling more social platforms, enhancing security measures, or implementing advanced marketing strategies.
________________________________________
By following this comprehensive guide for Stage 14: Social Media & Marketing Integration, you've successfully enhanced your eCommerce platform with robust social media and marketing functionalities. These integrations not only improve user engagement and streamline marketing efforts but also lay the groundwork for scaling your platform effectively.
Next Steps:
Proceed to Stage 15: Inventory Management Integration to further refine your backend services, ensuring efficient stock tracking and synchronization across your platform.
Stage 15: Custom AI Integration (e.g., GPT, DALL·E 3) is a transformative phase that introduces intelligent features to your eCommerce platform. Integrating AI functionalities like GPT-based chatbots and DALL·E 3 image generation can significantly enhance user experience, streamline operations, and drive engagement.
This comprehensive guide will walk you through setting up these AI-powered features, providing all necessary code, dependencies, configuration files, and documentation to ensure easy integration, even for beginners.
________________________________________
Stage 15: Custom AI Integration (e.g., GPT, DALL·E 3)
________________________________________
1. Overview
Objective:
• GPT-Based Chatbot: Implement a conversational AI assistant to handle customer inquiries, provide product recommendations, and assist with orders.
• DALL·E 3 Image Generation: Enable users to create custom product images or visualize products in different settings using AI-generated images.
Benefits:
• Enhanced Customer Support: Provides instant, 24/7 assistance, reducing response times and improving customer satisfaction.
• Personalized Shopping Experience: Offers tailored product recommendations based on user interactions and preferences.
• Creative Product Customization: Allows users to visualize products in various styles or configurations, boosting engagement and sales.
• Operational Efficiency: Automates routine tasks, freeing up resources for more strategic initiatives.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js – For building scalable API endpoints.
• Frontend Framework: React.js – For creating interactive user interfaces.
• State Management: Redux – For managing application state.
• HTTP Client: Axios – For making API requests.
• UI Library: Material-UI (MUI) – For pre-built UI components and styling.
• AI Services: OpenAI API – For integrating GPT-based chatbots and DALL·E 3 image generation.
• Environment Variables Management: dotenv – For securely managing API keys and secrets.
• Authentication: JWT (JSON Web Tokens) – For securing API endpoints.
• Form Handling: Formik and Yup – For form management and validation.
• Notifications: React Toastify – For displaying notifications to users.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Version Control: Git – Continuation from previous stages.
• Documentation: Markdown and Swagger – For API documentation.
________________________________________
3. Implementation Steps
3.1. Setting Up OpenAI API
To integrate GPT and DALL·E 3, you'll utilize OpenAI's APIs. Ensure you have an OpenAI account and obtain your API key.
1. Obtain OpenAI API Key:
o Sign up or log in to your OpenAI account.
o Navigate to the API section and generate a new API key.
o Important: Keep your API key secure and do not expose it in client-side code.
2. Configure Environment Variables:
Update the backend and frontend .env files to include your OpenAI API key.
Backend (auth-service/.env or create a new service for AI):
env
Copy code
OPENAI_API_KEY=your_openai_api_key
Frontend (frontend/user-dashboard/.env):
Since the API key should remain confidential, all OpenAI API interactions will be handled via the backend.
3.2. Backend: Create AI Service
To securely interact with OpenAI APIs, create a dedicated AI Service within your microservices architecture.
1. Create AI Service Directory:
bash
Copy code
mkdir ai-service
cd ai-service
npm init -y
2. Install Dependencies:
bash
Copy code
npm install express dotenv axios cors
npm install --save-dev nodemon eslint prettier jest supertest
3. Project Structure:
Organize the AI Service as follows:
bash
Copy code
ai-service/
├── controllers/
│ ├── aiController.js
├── routes/
│ ├── aiRoutes.js
├── utils/
│ ├── openai.js
├── tests/
│ ├── aiController.test.js
├── .env
├── .gitignore
├── index.js
├── package.json
├── README.md
└── ...
4. Configuration Files:
o .gitignore
gitignore
Copy code
# dependencies
/node_modules
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
o .env
env
Copy code
PORT=5007
NODE_ENV=development
OPENAI_API_KEY=your_openai_api_key
Note: Replace your_openai_api_key with your actual OpenAI API key.
o package.json
json
Copy code
{
"name": "ai-service",
"version": "1.0.0",
"description": "AI Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
}
}
5. Set Up Express Server (index.js):
javascript
Copy code
// ai-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const aiRoutes = require('./routes/aiRoutes');
// Load environment variables
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/ai', aiRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('AI Service API');
});
// Start Server
const PORT = process.env.PORT || 5007;
app.listen(PORT, () => {
console.log(`AI Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
6. OpenAI Utility (utils/openai.js):
Create a utility to interact with OpenAI's API.
javascript
Copy code
// ai-service/utils/openai.js
const axios = require('axios');
const dotenv = require('dotenv');
dotenv.config();
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_API_URL = 'https://api.openai.com/v1';
const generateChatResponse = async (prompt) => {
try {
const response = await axios.post(
`${OPENAI_API_URL}/chat/completions`,
{
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
max_tokens: 150,
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
return response.data.choices[0].message.content.trim();
} catch (error) {
console.error('Error generating chat response:', error.response.data);
throw new Error('Failed to generate response from AI.');
}
};
const generateImage = async (description) => {
try {
const response = await axios.post(
`${OPENAI_API_URL}/images/generations`,
{
prompt: description,
n: 1,
size: '512x512',
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
return response.data.data[0].url;
} catch (error) {
console.error('Error generating image:', error.response.data);
throw new Error('Failed to generate image from AI.');
}
};
module.exports = {
generateChatResponse,
generateImage,
};
7. AI Controller (controllers/aiController.js):
Handle AI-related requests.
javascript
Copy code
// ai-service/controllers/aiController.js
const { generateChatResponse, generateImage } = require('../utils/openai');
// GPT-Based Chatbot
const chat = async (req, res) => {
const { message } = req.body;
if (!message) {
return res.status(400).json({ message: 'Message is required' });
}
try {
const aiResponse = await generateChatResponse(message);
res.status(200).json({ response: aiResponse });
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// DALL·E 3 Image Generation
const createImage = async (req, res) => {
const { description } = req.body;
if (!description) {
return res.status(400).json({ message: 'Description is required' });
}
try {
const imageUrl = await generateImage(description);
res.status(200).json({ imageUrl });
} catch (error) {
res.status(500).json({ message: error.message });
}
};
module.exports = {
chat,
createImage,
};
8. AI Routes (routes/aiRoutes.js):
Define API endpoints for AI functionalities.
javascript
Copy code
// ai-service/routes/aiRoutes.js
const express = require('express');
const { chat, createImage } = require('../controllers/aiController');
const router = express.Router();
// Route: POST /api/ai/chat
router.post('/chat', chat);
// Route: POST /api/ai/image
router.post('/image', createImage);
module.exports = router;
9. Testing AI Service:
o tests/aiController.test.js
javascript
Copy code
// ai-service/tests/aiController.test.js
const request = require('supertest');
const app = require('../index'); // Import the Express app
const { generateChatResponse, generateImage } = require('../utils/openai');
jest.mock('../utils/openai');
describe('AI Controller', () => {
describe('POST /api/ai/chat', () => {
it('should return AI response for valid message', async () => {
const mockResponse = 'Hello! How can I assist you today?';
generateChatResponse.mockResolvedValue(mockResponse);
const res = await request(app)
.post('/api/ai/chat')
.send({ message: 'Hi there!' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('response', mockResponse);
});
it('should return 400 for missing message', async () => {
const res = await request(app).post('/api/ai/chat').send({});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Message is required');
});
it('should handle server errors gracefully', async () => {
generateChatResponse.mockRejectedValue(new Error('AI service error'));
const res = await request(app)
.post('/api/ai/chat')
.send({ message: 'Hi!' });
expect(res.statusCode).toEqual(500);
expect(res.body).toHaveProperty('message', 'AI service error');
});
});
describe('POST /api/ai/image', () => {
it('should return image URL for valid description', async () => {
const mockImageUrl = 'https://example.com/image.png';
generateImage.mockResolvedValue(mockImageUrl);
const res = await request(app)
.post('/api/ai/image')
.send({ description: 'A futuristic city skyline at sunset.' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('imageUrl', mockImageUrl);
});
it('should return 400 for missing description', async () => {
const res = await request(app).post('/api/ai/image').send({});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Description is required');
});
it('should handle server errors gracefully', async () => {
generateImage.mockRejectedValue(new Error('AI image generation error'));
const res = await request(app)
.post('/api/ai/image')
.send({ description: 'An abstract art piece.' });
expect(res.statusCode).toEqual(500);
expect(res.body).toHaveProperty('message', 'AI image generation error');
});
});
});
10. Run AI Service Tests:
bash
Copy code
cd ai-service
npm test
11. Dockerize AI Service:
o Create Dockerfile in AI Service Directory:
dockerfile
Copy code
# ai-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 5007
# Start the service
CMD [ "node", "index.js" ]
3.3. Frontend: Integrate AI Features
Integrate the AI functionalities into the frontend, enabling users to interact with the chatbot and generate images.
1. Update Environment Variables:
Ensure the frontend .env file includes the AI Service URL.
env
Copy code
REACT_APP_AI_SERVICE_URL=http://localhost:5007/api/ai
2. Install Dependencies:
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install @mui/icons-material
3. Create Chatbot Component (src/components/Chatbot/Chatbot.js):
javascript
Copy code
// src/components/Chatbot/Chatbot.js
import React, { useState } from 'react';
import {
Box,
TextField,
Button,
Typography,
Paper,
CircularProgress,
} from '@mui/material';
import axios from 'axios';
import { toast } from 'react-toastify';
const AIServiceURL = process.env.REACT_APP_AI_SERVICE_URL;
const Chatbot = () => {
const [message, setMessage] = useState('');
const [chatHistory, setChatHistory] = useState([]);
const [loading, setLoading] = useState(false);
const handleSend = async () => {
if (!message.trim()) return;
const userMessage = { sender: 'user', text: message };
setChatHistory([...chatHistory, userMessage]);
setMessage('');
setLoading(true);
try {
const response = await axios.post(`${AIServiceURL}/chat`, { message });
const aiMessage = { sender: 'ai', text: response.data.response };
setChatHistory((prev) => [...prev, aiMessage]);
} catch (error) {
toast.error('Failed to get response from AI.');
const aiMessage = { sender: 'ai', text: 'Sorry, something went wrong. Please try again.' };
setChatHistory((prev) => [...prev, aiMessage]);
} finally {
setLoading(false);
}
};
return (
<Box sx={{ mt: 4, p: 2 }}>
<Typography variant="h6" gutterBottom>
Chat with our AI Assistant
</Typography>
<Paper sx={{ maxHeight: 300, overflow: 'auto', p: 2, mb: 2 }}>
{chatHistory.map((chat, index) => (
<Box
key={index}
sx={{
display: 'flex',
justifyContent: chat.sender === 'user' ? 'flex-end' : 'flex-start',
mb: 1,
}}
>
<Box
sx={{
bgcolor: chat.sender === 'user' ? 'primary.main' : 'grey.300',
color: chat.sender === 'user' ? 'white' : 'black',
p: 1,
borderRadius: 2,
maxWidth: '80%',
}}
>
<Typography variant="body1">{chat.text}</Typography>
</Box>
</Box>
))}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'flex-start', mb: 1 }}>
<CircularProgress size={24} />
</Box>
)}
</Paper>
<Box sx={{ display: 'flex' }}>
<TextField
fullWidth
variant="outlined"
label="Type your message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
}}
/>
<Button variant="contained" color="primary" sx={{ ml: 1 }} onClick={handleSend} disabled={loading}>
Send
</Button>
</Box>
</Box>
);
};
export default Chatbot;
4. Create Image Generator Component (src/components/ImageGenerator/ImageGenerator.js):
javascript
Copy code
// src/components/ImageGenerator/ImageGenerator.js
import React, { useState } from 'react';
import {
Box,
TextField,
Button,
Typography,
CircularProgress,
Card,
CardMedia,
} from '@mui/material';
import axios from 'axios';
import { toast } from 'react-toastify';
const AIServiceURL = process.env.REACT_APP_AI_SERVICE_URL;
const ImageGenerator = () => {
const [description, setDescription] = useState('');
const [imageUrl, setImageUrl] = useState('');
const [loading, setLoading] = useState(false);
const handleGenerate = async () => {
if (!description.trim()) return;
setLoading(true);
setImageUrl('');
try {
const response = await axios.post(`${AIServiceURL}/image`, { description });
setImageUrl(response.data.imageUrl);
} catch (error) {
toast.error('Failed to generate image.');
} finally {
setLoading(false);
}
};
return (
<Box sx={{ mt: 4, p: 2 }}>
<Typography variant="h6" gutterBottom>
Generate Custom Images with AI
</Typography>
<TextField
fullWidth
variant="outlined"
label="Describe the image you want to create..."
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleGenerate();
}
}}
/>
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={handleGenerate} disabled={loading}>
{loading ? <CircularProgress size={24} /> : 'Generate Image'}
</Button>
{imageUrl && (
<Card sx={{ mt: 2, maxWidth: 512 }}>
<CardMedia component="img" height="512" image={imageUrl} alt="Generated AI" />
</Card>
)}
</Box>
);
};
export default ImageGenerator;
5. Integrate AI Components into Dashboard:
Update the Dashboard page to include the Chatbot and Image Generator components.
javascript
Copy code
// src/pages/Dashboard/Dashboard.js
import React from 'react';
import { Grid, Paper, Typography, Box } from '@mui/material';
import Orders from '../../components/Dashboard/Orders';
import Shipments from '../../components/Dashboard/Shipments';
import Payments from '../../components/Dashboard/Payments';
import Chatbot from '../../components/Chatbot/Chatbot';
import ImageGenerator from '../../components/ImageGenerator/ImageGenerator';
const Dashboard = () => {
const user = useSelector((state) => state.auth.user);
return (
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
Welcome, {user.name}!
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Orders />
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Shipments />
</Paper>
</Grid>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<Payments />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<Chatbot />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<ImageGenerator />
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default Dashboard;
6. Update Redux Store:
Since AI functionalities don't require specific state management beyond what's already handled, no changes to the Redux store are necessary. However, ensure that Axios base URLs are correctly set up if needed.
7. Testing Frontend AI Features:
o Chatbot Component Test (src/components/Chatbot/Chatbot.test.js):
javascript
Copy code
// src/components/Chatbot/Chatbot.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Chatbot from './Chatbot';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('Chatbot Component', () => {
test('renders chatbot interface', () => {
render(
<Provider store={store}>
<Chatbot />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Chat with our AI Assistant/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Type your message/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /send/i })).toBeInTheDocument();
});
test('sends message and receives AI response', async () => {
const userMessage = 'Hello, AI!';
const aiResponse = 'Hello! How can I assist you today?';
axios.post.mockResolvedValue({ data: { response: aiResponse } });
render(
<Provider store={store}>
<Chatbot />
<ToastContainer />
</Provider>
);
fireEvent.change(screen.getByLabelText(/Type your message/i), {
target: { value: userMessage },
});
fireEvent.click(screen.getByRole('button', { name: /send/i }));
expect(screen.getByText(userMessage)).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(aiResponse)).toBeInTheDocument();
});
});
test('handles AI response errors gracefully', async () => {
const userMessage = 'Tell me a joke.';
axios.post.mockRejectedValue(new Error('AI error'));
render(
<Provider store={store}>
<Chatbot />
<ToastContainer />
</Provider>
);
fireEvent.change(screen.getByLabelText(/Type your message/i), {
target: { value: userMessage },
});
fireEvent.click(screen.getByRole('button', { name: /send/i }));
expect(screen.getByText(userMessage)).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Sorry, something went wrong/i)).toBeInTheDocument();
});
});
});
o Image Generator Component Test (src/components/ImageGenerator/ImageGenerator.test.js):
javascript
Copy code
// src/components/ImageGenerator/ImageGenerator.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ImageGenerator from './ImageGenerator';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('ImageGenerator Component', () => {
test('renders image generator interface', () => {
render(
<Provider store={store}>
<ImageGenerator />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Generate Custom Images with AI/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Describe the image/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /generate image/i })).toBeInTheDocument();
});
test('generates image for valid description', async () => {
const description = 'A serene landscape with mountains and a river.';
const imageUrl = 'https://example.com/image.png';
axios.post.mockResolvedValue({ data: { imageUrl } });
render(
<Provider store={store}>
<ImageGenerator />
<ToastContainer />
</Provider>
);
fireEvent.change(screen.getByLabelText(/Describe the image/i), {
target: { value: description },
});
fireEvent.click(screen.getByRole('button', { name: /generate image/i }));
expect(screen.getByRole('progressbar')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByAltText(/Generated AI/i)).toBeInTheDocument();
expect(screen.getByRole('img', { name: /Generated AI/i })).toHaveAttribute('src', imageUrl);
});
});
test('handles image generation errors gracefully', async () => {
const description = 'An abstract art piece.';
axios.post.mockRejectedValue(new Error('AI image generation error'));
render(
<Provider store={store}>
<ImageGenerator />
<ToastContainer />
</Provider>
);
fireEvent.change(screen.getByLabelText(/Describe the image/i), {
target: { value: description },
});
fireEvent.click(screen.getByRole('button', { name: /generate image/i }));
expect(screen.getByRole('progressbar')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByAltText(/Generated AI/i)).not.toBeInTheDocument();
expect(screen.getByText(/Failed to generate image/i)).toBeInTheDocument();
});
});
});
8. Update Redux Store (if needed):
Since AI functionalities are handled independently, no specific Redux state management is required beyond existing configurations.
9. Dockerize Frontend with AI Integration:
Ensure that the frontend's Dockerfile and docker-compose configurations account for the AI Service.
o Update docker-compose.yml:
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
# ... existing services
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://localhost:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
- REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
# ... other services
volumes:
mongo-data:
Note: Within Docker Compose, services can communicate using service names. Update the REACT_APP_AI_SERVICE_URL to use the Docker service name (ai-service) instead of localhost.
10. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend accessible at http://localhost:3000/
o AI Service accessible at http://localhost:5007/api/ai
3.4. Documentation and Usage
1. Create README.md for AI Service:
markdown
Copy code
# AI Service
## Overview
The **AI Service** integrates OpenAI's GPT and DALL·E 3 APIs to provide intelligent chatbot functionalities and AI-generated images for the iBranDIDit.com platform.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Access to [OpenAI API](https://openai.com/api/)
- Existing microservices setup (Auth, Order, Payment, Shipping)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the AI Service Directory:**
```bash
cd path/to/ibrandidit-microservices/ai-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the ai-service/ directory based on the provided .env.example.
env
Copy code
PORT=5007
NODE_ENV=development
OPENAI_API_KEY=your_openai_api_key
Descriptions:
o PORT: Port on which the AI Service runs.
o NODE_ENV: Environment mode (development or production).
o OPENAI_API_KEY: Your OpenAI API key for accessing GPT and DALL·E 3 functionalities.
Note: Replace your_openai_api_key with your actual OpenAI API key.
Running the Service
Option 1: Running Locally
3. Start the AI Service:
bash
Copy code
npm run dev
The service will run on http://localhost:5007/.
Option 2: Using Docker
4. Build and Start the Service with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
Ensure that the AI Service is included in your docker-compose.yml.
API Documentation
The AI Service exposes the following endpoints:
1. GPT-Based Chatbot
o Endpoint: POST /api/ai/chat
o Description: Generates a conversational AI response based on user input.
o Request Body:
json
Copy code
{
"message": "User's message here"
}
o Response:
json
Copy code
{
"response": "AI-generated response here"
}
2. DALL·E 3 Image Generation
o Endpoint: POST /api/ai/image
o Description: Generates an image based on the provided description.
o Request Body:
json
Copy code
{
"description": "Description of the image to generate"
}
o Response:
json
Copy code
{
"imageUrl": "https://generated-image-url.com/image.png"
}
Testing
5. Run Tests:
bash
Copy code
npm test
This command runs all tests using Jest and generates coverage reports.
6. Test Coverage:
After running tests, a coverage/ directory is created containing detailed coverage reports.
Linting and Formatting
Maintain code quality using ESLint and Prettier.
7. Run Linting:
bash
Copy code
npm run lint
8. Run Formatting:
bash
Copy code
npm run format
Deployment
Option 1: Docker Deployment
9. Build the Docker Image:
bash
Copy code
cd ai-service
docker build -t ai-service .
10. Run the Docker Container:
bash
Copy code
docker run -d -p 5007:5007 --env-file .env ai-service
The AI Service will be accessible at http://localhost:5007/api/ai.
Option 2: Deploying to Cloud Providers
o Heroku, AWS, Google Cloud, etc.: Follow the respective provider's guidelines to deploy Node.js applications. Ensure environment variables are securely set.
Contribution Guidelines
11. Fork the Repository:
Create a personal fork of the project repository.
12. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/ai-integration
13. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: integrate GPT and DALL·E 3 functionalities"
14. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/ai-integration
Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
Copy code
2. Create README.md for Frontend AI Integration:
Update the README.md in the frontend/user-dashboard directory to include information about the AI features.
markdown
Copy code
# User Dashboard
## Overview
The **User Dashboard** is a React.js frontend application that provides users with a centralized platform to manage their profiles, view order history, track shipments, manage payments, interact with AI-powered features like chatbots and image generation, subscribe to newsletters, and engage with social media. This dashboard integrates seamlessly with backend microservices, ensuring a smooth and secure user experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Application](#running-the-application)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Backend Microservices running (Auth, Order, Payment, Shipping, AI)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Frontend Directory:**
```bash
cd path/to/ibrandidit-microservices/frontend/user-dashboard
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-dashboard/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:5004
REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://localhost:5007/api/ai
Descriptions:
o REACT_APP_API_BASE_URL: Base URL for the Order Service.
o REACT_APP_SHIPPING_SERVICE_URL: Base URL for the Shipping Service.
o REACT_APP_PAYMENT_SERVICE_URL: Base URL for the Payment Service.
o REACT_APP_AUTH_SERVICE_URL: Base URL for the Auth Service.
o REACT_APP_GOOGLE_ANALYTICS_ID: Your Google Analytics Tracking ID.
o REACT_APP_AI_SERVICE_URL: Base URL for the AI Service.
Note: Replace the URLs with actual service endpoints if using Docker Compose with service names (e.g., http://ai-service:5007/api/ai).
Running the Application
Option 1: Running Locally
3. Start Backend Microservices:
Ensure that all backend services (Auth, Order, Payment, Shipping, AI) are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
4. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
The application will run on http://localhost:3000/.
Login/Register to access the dashboard.
Option 2: Using Docker
5. Ensure AI Service is Included in Docker Compose:
Verify that the docker-compose.yml includes the AI Service with correct environment variables.
6. Start All Services with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
Frontend accessible at http://localhost:3000/.
API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
o Auth Service: http://localhost:5003/api-docs
o Order Service: http://localhost:5004/api-docs
o Payment Service: http://localhost:5005/api-docs
o Shipping Service: http://localhost:5006/api-docs
o AI Service: http://localhost:5007/api-docs
Testing
Testing ensures that the AI integrations work as expected.
Backend Testing
7. AI Service Tests:
bash
Copy code
cd ai-service
npm test
Frontend Testing
8. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
Ensure all tests pass, especially those related to chatbot and image generation functionalities.
Linting and Formatting
Maintain code quality using ESLint and Prettier.
9. Run Linting:
bash
Copy code
cd frontend/user-dashboard
npm run lint
10. Run Formatting:
bash
Copy code
cd frontend/user-dashboard
npm run format
Deployment
11. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
12. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
The application will run on http://localhost:5000/ by default.
13. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
Frontend will be accessible at http://localhost:3000/.
Ensure that all environment variables are correctly set in the .env files.
Replace placeholder values with actual credentials before deploying to production.
Contribution Guidelines
14. Fork the Repository:
Create a personal fork of the project repository.
15. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/ai-integration
16. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: integrate GPT-based chatbot and DALL·E 3 image generation"
17. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/ai-integration
Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
Copy code
3.5. Secure Environment Variables
Ensure that sensitive information like API keys are not exposed in client-side code. Only the backend should interact with OpenAI APIs.
1. Backend .env File:
env
Copy code
PORT=5007
NODE_ENV=development
OPENAI_API_KEY=your_openai_api_key
2. Frontend .env File:
env
Copy code
REACT_APP_API_BASE_URL=http://localhost:5004
REACT_APP_SHIPPING_SERVICE_URL=http://localhost:5006
REACT_APP_PAYMENT_SERVICE_URL=http://localhost:5005
REACT_APP_AUTH_SERVICE_URL=http://localhost:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://localhost:5007/api/ai
Note: Replace your_openai_api_key and G-XXXXXXXXXX with your actual OpenAI API key and Google Analytics ID, respectively.
3.6. Best Practices
• Rate Limiting: Implement rate limiting on AI Service endpoints to prevent abuse and manage API usage costs.
• Caching: Cache frequent AI responses to reduce API calls and improve response times.
• Error Handling: Implement comprehensive error handling to manage API failures gracefully.
• Logging: Use logging libraries like winston to monitor AI Service performance and errors.
________________________________________
4. Complete Code and Configuration Files
4.1. Backend: AI Service
1. ai-service/package.json
json
Copy code
{
"name": "ai-service",
"version": "1.0.0",
"description": "AI Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"axios": "^1.4.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
}
}
2. ai-service/index.js
javascript
Copy code
// ai-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const aiRoutes = require('./routes/aiRoutes');
// Load environment variables
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/ai', aiRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('AI Service API');
});
// Start Server
const PORT = process.env.PORT || 5007;
app.listen(PORT, () => {
console.log(`AI Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
3. ai-service/utils/openai.js
javascript
Copy code
// ai-service/utils/openai.js
const axios = require('axios');
const dotenv = require('dotenv');
dotenv.config();
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_API_URL = 'https://api.openai.com/v1';
const generateChatResponse = async (prompt) => {
try {
const response = await axios.post(
`${OPENAI_API_URL}/chat/completions`,
{
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
max_tokens: 150,
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
return response.data.choices[0].message.content.trim();
} catch (error) {
console.error('Error generating chat response:', error.response.data);
throw new Error('Failed to generate response from AI.');
}
};
const generateImage = async (description) => {
try {
const response = await axios.post(
`${OPENAI_API_URL}/images/generations`,
{
prompt: description,
n: 1,
size: '512x512',
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
return response.data.data[0].url;
} catch (error) {
console.error('Error generating image:', error.response.data);
throw new Error('Failed to generate image from AI.');
}
};
module.exports = {
generateChatResponse,
generateImage,
};
4. ai-service/controllers/aiController.js
javascript
Copy code
// ai-service/controllers/aiController.js
const { generateChatResponse, generateImage } = require('../utils/openai');
// GPT-Based Chatbot
const chat = async (req, res) => {
const { message } = req.body;
if (!message) {
return res.status(400).json({ message: 'Message is required' });
}
try {
const aiResponse = await generateChatResponse(message);
res.status(200).json({ response: aiResponse });
} catch (error) {
res.status(500).json({ message: error.message });
}
};
// DALL·E 3 Image Generation
const createImage = async (req, res) => {
const { description } = req.body;
if (!description) {
return res.status(400).json({ message: 'Description is required' });
}
try {
const imageUrl = await generateImage(description);
res.status(200).json({ imageUrl });
} catch (error) {
res.status(500).json({ message: error.message });
}
};
module.exports = {
chat,
createImage,
};
5. ai-service/routes/aiRoutes.js
javascript
Copy code
// ai-service/routes/aiRoutes.js
const express = require('express');
const { chat, createImage } = require('../controllers/aiController');
const router = express.Router();
// Route: POST /api/ai/chat
router.post('/chat', chat);
// Route: POST /api/ai/image
router.post('/image', createImage);
module.exports = router;
6. ai-service/tests/aiController.test.js
javascript
Copy code
// ai-service/tests/aiController.test.js
const request = require('supertest');
const app = require('../index'); // Import the Express app
const { generateChatResponse, generateImage } = require('../utils/openai');
jest.mock('../utils/openai');
describe('AI Controller', () => {
describe('POST /api/ai/chat', () => {
it('should return AI response for valid message', async () => {
const mockResponse = 'Hello! How can I assist you today?';
generateChatResponse.mockResolvedValue(mockResponse);
const res = await request(app)
.post('/api/ai/chat')
.send({ message: 'Hi there!' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('response', mockResponse);
});
it('should return 400 for missing message', async () => {
const res = await request(app).post('/api/ai/chat').send({});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Message is required');
});
it('should handle server errors gracefully', async () => {
generateChatResponse.mockRejectedValue(new Error('AI service error'));
const res = await request(app)
.post('/api/ai/chat')
.send({ message: 'Hi!' });
expect(res.statusCode).toEqual(500);
expect(res.body).toHaveProperty('message', 'AI service error');
});
});
describe('POST /api/ai/image', () => {
it('should return image URL for valid description', async () => {
const mockImageUrl = 'https://example.com/image.png';
generateImage.mockResolvedValue(mockImageUrl);
const res = await request(app)
.post('/api/ai/image')
.send({ description: 'A futuristic city skyline at sunset.' });
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('imageUrl', mockImageUrl);
});
it('should return 400 for missing description', async () => {
const res = await request(app).post('/api/ai/image').send({});
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'Description is required');
});
it('should handle server errors gracefully', async () => {
generateImage.mockRejectedValue(new Error('AI image generation error'));
const res = await request(app)
.post('/api/ai/image')
.send({ description: 'An abstract art piece.' });
expect(res.statusCode).toEqual(500);
expect(res.body).toHaveProperty('message', 'AI image generation error');
});
});
});
7. ai-service/Dockerfile
dockerfile
Copy code
# ai-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Expose port
EXPOSE 5007
# Start the service
CMD [ "node", "index.js" ]
4.2. Frontend: User Dashboard
1. frontend/user-dashboard/package.json
(Already provided in previous stages)
2. frontend/user-dashboard/src/redux/slices/authSlice.js
(Already provided in previous stages)
3. frontend/user-dashboard/src/redux/slices/orderSlice.js
(Already provided in previous stages)
4. frontend/user-dashboard/src/redux/slices/shipmentSlice.js
(Already provided in previous stages)
5. frontend/user-dashboard/src/redux/slices/paymentSlice.js
(Already provided in previous stages)
6. frontend/user-dashboard/src/redux/store.js
(Already provided in previous stages)
7. frontend/user-dashboard/src/services/api.js
(Already provided in previous stages)
8. frontend/user-dashboard/src/services/analytics.js
(Already provided in previous stages)
9. frontend/user-dashboard/src/components/Layout/Layout.js
(Already provided in previous stages)
10. frontend/user-dashboard/src/components/PrivateRoute/PrivateRoute.js
(Already provided in previous stages)
11. frontend/user-dashboard/src/components/Dashboard/Orders.js
(Already provided in previous stages)
12. frontend/user-dashboard/src/components/Dashboard/Shipments.js
(Already provided in previous stages)
13. frontend/user-dashboard/src/components/Dashboard/Payments.js
(Already provided in previous stages)
14. frontend/user-dashboard/src/components/SocialShare/SocialShare.js
(Already provided in previous stages)
15. frontend/user-dashboard/src/components/Newsletter/Newsletter.js
(Already provided in previous stages)
16. frontend/user-dashboard/src/pages/AuthCallback/AuthCallback.js
(Already provided in previous stages)
17. frontend/user-dashboard/src/pages/Home/Home.js
(Already provided in previous stages)
18. frontend/user-dashboard/src/pages/Login/Login.js
(Already provided in previous stages)
19. frontend/user-dashboard/src/pages/Register/Register.js
(Already provided in previous stages)
20. frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided in previous stages)
21. frontend/user-dashboard/src/pages/Profile/Profile.js
(Already provided in previous stages)
22. frontend/user-dashboard/src/pages/ProductDetail/ProductDetail.js
(Already provided in previous stages)
23. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided in previous stages)
24. frontend/user-dashboard/src/App.js
(Already provided in previous stages)
25. frontend/user-dashboard/src/index.js
(Already provided in previous stages)
26. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided in previous stages)
27. frontend/user-dashboard/src/pages/AuthCallback/AuthCallback.js
(Already provided in previous stages)
28. frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
29. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
30. frontend/user-dashboard/src/components/Chatbot/Chatbot.test.js
(Already provided above)
31. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.test.js
(Already provided above)
4.3. Docker Configuration
1. ibrandidit-microservices/docker-compose.yml
Update your docker-compose.yml to include the AI Service.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
# ... other services
volumes:
mongo-data:
Notes:
o Service Names: Within Docker Compose, services can communicate using their service names. For example, the frontend can access the AI Service at http://ai-service:5007/api/ai.
o Environment Variables: Ensure that all necessary environment variables are correctly set for each service.
2. Build and Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend accessible at http://localhost:3000/
o AI Service accessible at http://localhost:5007/api/ai
4.4. Frontend Components
1. frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
2. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
3. Update frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided above)
4. Update frontend/user-dashboard/src/App.js
Ensure that all routes are correctly set up.
javascript
Copy code
// src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Layout from './components/Layout/Layout';
import Home from './pages/Home/Home';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Dashboard from './pages/Dashboard/Dashboard';
import Profile from './pages/Profile/Profile';
import Orders from './pages/Orders/Orders';
import Shipments from './pages/Shipments/Shipments';
import Payments from './pages/Payments/Payments';
import AuthCallback from './pages/AuthCallback/AuthCallback';
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
function App() {
const token = useSelector((state) => state.auth.token);
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/login"
element={!token ? <Login /> : <Navigate to="/dashboard" />}
/>
<Route
path="/register"
element={!token ? <Register /> : <Navigate to="/dashboard" />}
/>
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/profile"
element={
<PrivateRoute>
<Profile />
</PrivateRoute>
}
/>
<Route
path="/orders"
element={
<PrivateRoute>
<Orders />
</PrivateRoute>
}
/>
<Route
path="/shipments"
element={
<PrivateRoute>
<Shipments />
</PrivateRoute>
}
/>
<Route
path="/payments"
element={
<PrivateRoute>
<Payments />
</PrivateRoute>
}
/>
<Route path="/auth/callback" element={<AuthCallback />} />
{/* Add more routes as needed */}
</Routes>
</Layout>
<ToastContainer />
</Router>
);
}
export default App;
4.5. Testing Frontend AI Features
1. Chatbot Component Test (frontend/user-dashboard/src/components/Chatbot/Chatbot.test.js)
(Already provided above)
2. Image Generator Component Test (frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.test.js)
(Already provided above)
3. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Ensure all tests pass, especially those related to AI functionalities.
4.6. Additional Configuration
1. Rate Limiting in AI Service (ai-service/index.js):
To prevent abuse and manage API usage costs, implement rate limiting.
bash
Copy code
npm install express-rate-limit
javascript
Copy code
// ai-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const aiRoutes = require('./routes/aiRoutes');
// Load environment variables
dotenv.config();
const app = express();
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/ai', aiRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('AI Service API');
});
// Start Server
const PORT = process.env.PORT || 5007;
app.listen(PORT, () => {
console.log(`AI Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
2. Caching AI Responses:
Implement caching for frequent AI responses to reduce API calls and improve response times.
bash
Copy code
npm install node-cache
javascript
Copy code
// ai-service/utils/openai.js
const axios = require('axios');
const dotenv = require('dotenv');
const NodeCache = require('node-cache');
dotenv.config();
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_API_URL = 'https://api.openai.com/v1';
const cache = new NodeCache({ stdTTL: 3600 }); // Cache for 1 hour
const generateChatResponse = async (prompt) => {
const cachedResponse = cache.get(`chat:${prompt}`);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await axios.post(
`${OPENAI_API_URL}/chat/completions`,
{
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
max_tokens: 150,
temperature: 0.7,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
const aiResponse = response.data.choices[0].message.content.trim();
cache.set(`chat:${prompt}`, aiResponse);
return aiResponse;
} catch (error) {
console.error('Error generating chat response:', error.response.data);
throw new Error('Failed to generate response from AI.');
}
};
const generateImage = async (description) => {
const cachedImage = cache.get(`image:${description}`);
if (cachedImage) {
return cachedImage;
}
try {
const response = await axios.post(
`${OPENAI_API_URL}/images/generations`,
{
prompt: description,
n: 1,
size: '512x512',
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
}
);
const imageUrl = response.data.data[0].url;
cache.set(`image:${description}`, imageUrl);
return imageUrl;
} catch (error) {
console.error('Error generating image:', error.response.data);
throw new Error('Failed to generate image from AI.');
}
};
module.exports = {
generateChatResponse,
generateImage,
};
3. Logging with Winston:
Implement logging to monitor AI Service performance and errors.
bash
Copy code
npm install winston
javascript
Copy code
// ai-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const aiRoutes = require('./routes/aiRoutes');
const winston = require('winston');
// Load environment variables
dotenv.config();
// Initialize Logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
// Add file transports if needed
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
// new winston.transports.File({ filename: 'combined.log' }),
],
});
const app = express();
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// Middleware
app.use(cors());
app.use(express.json());
// Routes
app.use('/api/ai', aiRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('AI Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Start Server
const PORT = process.env.PORT || 5007;
app.listen(PORT, () => {
logger.info(`AI Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
4.4. Frontend: User Dashboard
1. frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
2. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
3. Update frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided above)
4. Update frontend/user-dashboard/src/App.js
(Already provided above)
5. frontend/user-dashboard/src/index.js
(Already provided above)
6. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided above)
7. Additional Components:
o frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
o frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
8. frontend/user-dashboard/src/components/Chatbot/Chatbot.test.js
(Already provided above)
9. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.test.js
(Already provided above)
4.5. Docker Configuration
Ensure that all services are correctly defined in docker-compose.yml.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
# ... other services
volumes:
mongo-data:
Notes:
• AI Service: Added with port 5007 and environment variable OPENAI_API_KEY.
• Frontend: Updated REACT_APP_AI_SERVICE_URL to point to ai-service within Docker network.
• Service Dependencies: Ensures services start in the correct order.
2. Build and Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend accessible at http://localhost:3000/
o AI Service accessible at http://localhost:5007/api/ai
________________________________________
5. Documentation and Usage
5.1. Running the AI Service Locally
1. Start the AI Service:
bash
Copy code
cd path/to/ibrandidit-microservices/ai-service
npm run dev
o The AI Service will run on http://localhost:5007/.
2. Ensure Backend Services Are Running:
o Auth, Order, Payment, Shipping services should be running and accessible.
5.2. Interacting with AI Features
1. GPT-Based Chatbot
• Accessing the Chatbot:
o Navigate to http://localhost:3000/dashboard after logging in.
o Locate the Chat with our AI Assistant section.
• Using the Chatbot:
o Type your message in the input field.
o Press Enter or click the Send button.
o The AI will respond based on your input.
• Features:
o Customer Support: Ask questions about products, orders, shipping, etc.
o Product Recommendations: Request personalized product suggestions.
o General Inquiries: Engage in casual conversations or seek assistance.
2. DALL·E 3 Image Generation
• Accessing Image Generator:
o Navigate to http://localhost:3000/dashboard after logging in.
o Locate the Generate Custom Images with AI section.
• Using the Image Generator:
o Enter a descriptive prompt of the image you want to create.
o Press Enter or click the Generate Image button.
o The AI will generate an image based on your description.
• Features:
o Product Customization: Visualize products in different styles or configurations.
o Creative Visualization: Generate images for promotional materials or social media.
o User Engagement: Encourage users to interact creatively with your platform.
5.3. API Documentation
The AI Service exposes the following endpoints:
1. GPT-Based Chatbot
o Endpoint: POST /api/ai/chat
o Description: Generates a conversational AI response based on user input.
o Request Body:
json
Copy code
{
"message": "User's message here"
}
o Response:
json
Copy code
{
"response": "AI-generated response here"
}
2. DALL·E 3 Image Generation
o Endpoint: POST /api/ai/image
o Description: Generates an image based on the provided description.
o Request Body:
json
Copy code
{
"description": "Description of the image to generate"
}
o Response:
json
Copy code
{
"imageUrl": "https://generated-image-url.com/image.png"
}
5.4. Testing
1. Backend Testing:
o Run AI Service Tests:
bash
Copy code
cd ai-service
npm test
o Expected Results:
All tests should pass, ensuring that AI functionalities work as intended.
2. Frontend Testing:
o Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Expected Results:
All tests should pass, especially those related to Chatbot and Image Generator components.
5.5. Linting and Formatting
Maintain code quality and consistency.
1. Run Linting:
bash
Copy code
cd frontend/user-dashboard
npm run lint
2. Run Formatting:
bash
Copy code
cd frontend/user-dashboard
npm run format
5.6. Deployment
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
o This command creates a production-ready build in the build/ directory.
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
o Ensure Docker Compose is Updated:
Verify that docker-compose.yml includes the AI Service and that all environment variables are correctly set.
o Build and Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Access the Application:
Frontend: http://localhost:3000/
AI Service: http://localhost:5007/api/ai
o Production Deployment:
Cloud Providers: Deploy to platforms like AWS, Google Cloud, Heroku, or DigitalOcean.
CI/CD Pipelines: Set up automated deployments using tools like GitHub Actions or Jenkins.
Security: Ensure HTTPS is enabled and environment variables are securely managed.
________________________________________
6. Integration with Previous Services
Ensure that the Custom AI Integration seamlessly interacts with existing microservices and frontend components.
6.1. Auth Service Integration
• Secure AI Service Endpoints:
o Implement authentication middleware in the AI Service to verify JWT tokens from the frontend.
bash
Copy code
npm install jsonwebtoken
o Update ai-service/index.js:
javascript
Copy code
// ai-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const aiRoutes = require('./routes/aiRoutes');
const winston = require('winston');
// Load environment variables
dotenv.config();
// Initialize Logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
],
});
const app = express();
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// Middleware
app.use(cors());
app.use(express.json());
// Authentication Middleware
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ message: 'Invalid token' });
}
};
// Apply authentication middleware to AI routes
app.use('/api/ai', authenticate, aiRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('AI Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Start Server
const PORT = process.env.PORT || 5007;
app.listen(PORT, () => {
logger.info(`AI Service running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
module.exports = app; // Export app for testing
o Explanation:
Authentication Middleware: Verifies JWT tokens to secure AI Service endpoints.
Usage: Only authenticated users can access /api/ai/chat and /api/ai/image endpoints.
6.2. Order, Shipping, and Payment Services Integration
• Personalized Recommendations:
o Utilize the GPT-based chatbot to provide personalized product recommendations based on user interactions and order history.
o Example:
javascript
Copy code
// frontend/user-dashboard/src/components/Chatbot/Chatbot.js
// ... existing code
const handleSend = async () => {
if (!message.trim()) return;
const userMessage = { sender: 'user', text: message };
setChatHistory([...chatHistory, userMessage]);
setMessage('');
setLoading(true);
try {
// If user requests recommendations
if (message.toLowerCase().includes('recommend')) {
const recommendations = await getRecommendations();
const aiMessage = { sender: 'ai', text: recommendations };
setChatHistory((prev) => [...prev, aiMessage]);
} else {
const response = await axios.post(`${AIServiceURL}/chat`, { message }, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const aiMessage = { sender: 'ai', text: response.data.response };
setChatHistory((prev) => [...prev, aiMessage]);
}
} catch (error) {
toast.error('Failed to get response from AI.');
const aiMessage = { sender: 'ai', text: 'Sorry, something went wrong. Please try again.' };
setChatHistory((prev) => [...prev, aiMessage]);
} finally {
setLoading(false);
}
};
const getRecommendations = async () => {
try {
const response = await axios.get(`${API_BASE_URL}/recommendations`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const products = response.data.products;
if (products.length === 0) {
return 'I couldn\'t find any recommendations for you at the moment.';
}
const productList = products.map((p, idx) => `${idx + 1}. ${p.name}`).join('\n');
return `Based on your interests, I recommend the following products:\n${productList}`;
} catch (error) {
console.error('Error fetching recommendations:', error);
return 'I\'m unable to provide recommendations right now.';
}
};
o Explanation:
Recommendation Endpoint: /recommendations should be an existing endpoint in the Order Service that returns personalized product suggestions based on user data.
Chatbot Logic: Detects if the user requests recommendations and fetches them accordingly.
6.3. Frontend Dashboard Integration
• Display Generated Images:
o Allow users to visualize custom product configurations using AI-generated images.
o Example Use Case:
Users can describe a customized version of a product, and the AI generates an image depicting their vision.
________________________________________
5. Additional Tools and Best Practices
1. Rate Limiting:
o Prevent abuse by limiting the number of AI requests per user/IP.
o Already implemented in AI Service using express-rate-limit.
2. Caching:
o Reduce API calls and improve response times by caching frequent AI responses.
o Implemented using node-cache in utils/openai.js.
3. Error Handling:
o Ensure that all errors are handled gracefully, providing meaningful feedback to users.
o Implemented in AI Service's error-handling middleware and frontend components.
4. Logging:
o Monitor service performance and debug issues using logs.
o Implemented using winston in AI Service.
5. Security:
o Keep API keys and sensitive data secure using environment variables.
o Ensure that AI Service endpoints are authenticated using JWT tokens.
6. Testing:
o Maintain high code quality and reliability through comprehensive testing.
o Implemented using Jest and React Testing Library.
7. Documentation:
o Keep all components and services well-documented for easy maintenance and onboarding.
o Utilize README.md and Swagger for API documentation.
8. Responsive Design:
o Ensure that AI features are accessible and functional across various devices and screen sizes.
o Utilize Material-UI's responsive grid system.
________________________________________
6. Troubleshooting Tips
1. AI Service Not Responding:
o Problem: Frontend AI features are not working, and AI Service endpoints are unresponsive.
o Solution:
Ensure that the AI Service is running (http://localhost:5007/api/ai).
Check Docker containers to verify that the AI Service container is up.
Review AI Service logs for any errors.
2. Authentication Errors:
o Problem: Frontend receives 401 Unauthorized errors when accessing AI Service endpoints.
o Solution:
Ensure that JWT tokens are correctly stored and included in the Authorization header.
Verify that the JWT_SECRET in the AI Service matches that of the Auth Service.
Confirm that the frontend is correctly configured to use the AI Service URL.
3. OpenAI API Errors:
o Problem: AI Service returns errors related to OpenAI API usage.
o Solution:
Check that the OPENAI_API_KEY is correctly set in the AI Service .env file.
Ensure that the API key has sufficient permissions and quota.
Review error messages from OpenAI to identify specific issues (e.g., rate limits, invalid requests).
4. CORS Issues:
o Problem: Frontend cannot communicate with AI Service due to CORS policy.
o Solution:
Ensure that the AI Service has CORS enabled (app.use(cors())).
Verify that the frontend origin is allowed in the AI Service's CORS configuration.
5. Chatbot Not Responding Correctly:
o Problem: AI responses are irrelevant or incorrect.
o Solution:
Review the prompts sent to the AI Service to ensure clarity and relevance.
Adjust AI parameters like temperature and max_tokens to fine-tune responses.
Monitor AI Service logs for any unexpected behavior.
6. Image Generation Failures:
o Problem: DALL·E 3 fails to generate images based on descriptions.
o Solution:
Ensure that the descriptions provided are clear and within the API's guidelines.
Check the AI Service logs for specific error messages.
Verify that the OpenAI API key has access to the DALL·E 3 functionality.
7. Rate Limiting Issues:
o Problem: Users receive rate limit errors after multiple AI requests.
o Solution:
Review and adjust rate limiting settings in the AI Service as needed.
Implement user-specific rate limits if necessary.
Inform users about rate limits and encourage responsible usage.
8. Deployment Challenges:
o Problem: AI features work locally but fail after deployment.
o Solution:
Ensure that environment variables are correctly set in the production environment.
Verify that the AI Service is accessible from the deployed frontend.
Check for network or firewall restrictions that might block AI Service communications.
________________________________________
7. Additional Resources
• OpenAI API Documentation: https://platform.openai.com/docs/api-reference/introduction
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Redux Documentation: https://redux.js.org/introduction/getting-started
• Material-UI Documentation: https://mui.com/getting-started/usage/
• Formik Documentation: https://formik.org/docs/overview
• Yup Documentation: https://github.com/jquense/yup
• Axios Documentation: https://axios-http.com/docs/intro
• React Toastify Documentation: https://fkhadra.github.io/react-toastify/introduction
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Passport.js Documentation: http://www.passportjs.org/docs/
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 15: Custom AI Integration within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced AI features, enhancing security measures, or optimizing performance.
________________________________________
By following this comprehensive guide for Stage 15: Custom AI Integration, you've successfully integrated powerful AI functionalities into your eCommerce platform. These features not only enhance user interaction and personalization but also set the stage for scalable and intelligent operations.
Next Steps:
Proceed to Stage 16: Inventory Management Integration to further refine your backend services, ensuring efficient stock tracking and synchronization across your platform.
Stage 16: Product & Template Customization is a pivotal phase that empowers users to personalize products and templates to their preferences. This stage enhances user engagement, satisfaction, and drives sales by offering tailored shopping experiences. Below is a comprehensive guide to implementing this stage, complete with code, dependencies, configuration files, and documentation suitable for beginners.
________________________________________
Stage 16: Product & Template Customization
________________________________________
1. Overview
Objective:
• Product Customization: Allow users to personalize products by selecting options, adding text, uploading images, or choosing styles.
• Template Customization: Enable users to customize templates (e.g., for digital products, designs) to fit their specific needs.
Benefits:
• Enhanced User Experience: Provides a personalized shopping experience, increasing customer satisfaction.
• Increased Engagement: Interactive customization features keep users engaged longer.
• Higher Conversion Rates: Personalized products are more likely to convert into sales.
• Operational Efficiency: Streamlines the customization process, reducing manual interventions.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js – For building scalable API endpoints.
• Frontend Framework: React.js – For creating interactive user interfaces.
• State Management: Redux – For managing application state.
• Database: MongoDB – For storing product and customization data.
• Object Data Modeling (ODM): Mongoose – For modeling and interacting with MongoDB.
• File Storage: Multer – For handling file uploads.
• Authentication: JWT (JSON Web Tokens) – For securing API endpoints.
• HTTP Client: Axios – For making API requests.
• UI Library: Material-UI (MUI) – For pre-built UI components and styling.
• Form Handling: Formik and Yup – For form management and validation.
• Notifications: React Toastify – For displaying notifications to users.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Version Control: Git – Continuation from previous stages.
• Documentation: Markdown and Swagger – For API documentation.
________________________________________
3. Implementation Steps
3.1. Backend: Create Product Customization Service
To manage product and template customizations, we'll create a dedicated Product Customization Service.
3.1.1. Set Up the Product Customization Service
1. Create the Service Directory:
bash
Copy code
mkdir product-customization-service
cd product-customization-service
npm init -y
2. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors multer
npm install --save-dev nodemon eslint prettier jest supertest
3. Project Structure:
Organize the service as follows:
bash
Copy code
product-customization-service/
├── controllers/
│ ├── customizationController.js
├── models/
│ ├── Product.js
│ ├── Template.js
│ ├── Customization.js
├── routes/
│ ├── customizationRoutes.js
├── uploads/
│ ├── (Uploaded files will be stored here)
├── utils/
│ ├── upload.js
├── tests/
│ ├── customizationController.test.js
├── .env
├── .gitignore
├── index.js
├── package.json
├── README.md
└── ...
3.1.2. Configuration Files
• .gitignore:
gitignore
Copy code
# dependencies
/node_modules
# production
/build
# uploads
/uploads
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
• .env:
env
Copy code
PORT=5008
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Note: Replace your_jwt_secret_key with your actual JWT secret key.
• package.json:
json
Copy code
{
"name": "product-customization-service",
"version": "1.0.0",
"description": "Product Customization Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
}
}
3.1.3. Define Data Models
1. Product Model (models/Product.js):
javascript
Copy code
// product-customization-service/models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
images: [
{
type: String, // URL or path to the image
},
],
availableOptions: [
{
optionName: String,
optionValues: [String],
},
],
templates: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Template',
},
],
},
{ timestamps: true }
);
module.exports = mongoose.model('Product', ProductSchema);
2. Template Model (models/Template.js):
javascript
Copy code
// product-customization-service/models/Template.js
const mongoose = require('mongoose');
const TemplateSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
customizableFields: [
{
fieldName: String,
fieldType: String, // e.g., text, color, image
options: [String], // For dropdowns or predefined options
},
],
},
{ timestamps: true }
);
module.exports = mongoose.model('Template', TemplateSchema);
3. Customization Model (models/Customization.js):
javascript
Copy code
// product-customization-service/models/Customization.js
const mongoose = require('mongoose');
const CustomizationSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true,
},
template: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Template',
},
selectedOptions: [
{
optionName: String,
optionValue: String,
},
],
customizedFields: [
{
fieldName: String,
fieldValue: String, // Can be text, color codes, image paths
},
],
image: String, // Path to the customized image if any
},
{ timestamps: true }
);
module.exports = mongoose.model('Customization', CustomizationSchema);
3.1.4. Set Up Express Server (index.js)
javascript
Copy code
// product-customization-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const customizationRoutes = require('./routes/customizationRoutes');
dotenv.config();
const app = express();
// Middleware
app.use(cors());
app.use(express.json());
// Static folder for uploads
app.use('/uploads', express.static('uploads'));
// Routes
app.use('/api/customizations', customizationRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Product Customization Service API');
});
// Connect to MongoDB
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('MongoDB Connected');
// Start Server
const PORT = process.env.PORT || 5008;
app.listen(PORT, () => {
console.log(`Product Customization Service running on port ${PORT}`);
});
})
.catch((err) => {
console.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
3.1.5. File Upload Handling (utils/upload.js)
javascript
Copy code
// product-customization-service/utils/upload.js
const multer = require('multer');
const path = require('path');
// Storage configuration
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // Uploads directory
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix + path.extname(file.originalname)); // Unique filename
},
});
// File filter for images
const fileFilter = (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error('Only images are allowed'));
}
};
// Initialize upload
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter,
});
module.exports = upload;
3.1.6. Customization Controller (controllers/customizationController.js)
javascript
Copy code
// product-customization-service/controllers/customizationController.js
const Customization = require('../models/Customization');
const Product = require('../models/Product');
const Template = require('../models/Template');
// Create a new customization
const createCustomization = async (req, res) => {
const { productId, templateId, selectedOptions, customizedFields } = req.body;
const userId = req.user.id; // Assumes authentication middleware sets req.user
try {
// Validate product
const product = await Product.findById(productId);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
// Validate template if provided
let template = null;
if (templateId) {
template = await Template.findById(templateId);
if (!template) {
return res.status(404).json({ message: 'Template not found' });
}
}
// Handle image upload if any
let imagePath = '';
if (req.file) {
imagePath = `/uploads/${req.file.filename}`;
}
const customization = new Customization({
user: userId,
product: productId,
template: templateId,
selectedOptions,
customizedFields,
image: imagePath,
});
await customization.save();
res.status(201).json({ message: 'Customization created successfully', customization });
} catch (error) {
console.error('Error creating customization:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get all customizations for a user
const getUserCustomizations = async (req, res) => {
const userId = req.user.id;
try {
const customizations = await Customization.find({ user: userId })
.populate('product')
.populate('template');
res.status(200).json({ customizations });
} catch (error) {
console.error('Error fetching customizations:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get a specific customization
const getCustomizationById = async (req, res) => {
const customizationId = req.params.id;
const userId = req.user.id;
try {
const customization = await Customization.findOne({ _id: customizationId, user: userId })
.populate('product')
.populate('template');
if (!customization) {
return res.status(404).json({ message: 'Customization not found' });
}
res.status(200).json({ customization });
} catch (error) {
console.error('Error fetching customization:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Update a customization
const updateCustomization = async (req, res) => {
const customizationId = req.params.id;
const userId = req.user.id;
const { selectedOptions, customizedFields } = req.body;
try {
const customization = await Customization.findOne({ _id: customizationId, user: userId });
if (!customization) {
return res.status(404).json({ message: 'Customization not found' });
}
// Update fields
if (selectedOptions) customization.selectedOptions = selectedOptions;
if (customizedFields) customization.customizedFields = customizedFields;
// Handle image upload if any
if (req.file) {
customization.image = `/uploads/${req.file.filename}`;
}
await customization.save();
res.status(200).json({ message: 'Customization updated successfully', customization });
} catch (error) {
console.error('Error updating customization:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Delete a customization
const deleteCustomization = async (req, res) => {
const customizationId = req.params.id;
const userId = req.user.id;
try {
const customization = await Customization.findOneAndDelete({ _id: customizationId, user: userId });
if (!customization) {
return res.status(404).json({ message: 'Customization not found' });
}
res.status(200).json({ message: 'Customization deleted successfully' });
} catch (error) {
console.error('Error deleting customization:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
createCustomization,
getUserCustomizations,
getCustomizationById,
updateCustomization,
deleteCustomization,
};
3.1.7. Customization Routes (routes/customizationRoutes.js)
javascript
Copy code
// product-customization-service/routes/customizationRoutes.js
const express = require('express');
const multer = require('multer');
const {
createCustomization,
getUserCustomizations,
getCustomizationById,
updateCustomization,
deleteCustomization,
} = require('../controllers/customizationController');
const upload = require('../utils/upload');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Apply authentication middleware
router.use(authenticate);
// Route: POST /api/customizations
router.post('/', upload.single('image'), createCustomization);
// Route: GET /api/customizations
router.get('/', getUserCustomizations);
// Route: GET /api/customizations/:id
router.get('/:id', getCustomizationById);
// Route: PUT /api/customizations/:id
router.put('/:id', upload.single('image'), updateCustomization);
// Route: DELETE /api/customizations/:id
router.delete('/:id', deleteCustomization);
module.exports = router;
3.1.8. Authentication Middleware (middleware/authenticate.js)
javascript
Copy code
// product-customization-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
3.1.9. Testing Backend API Endpoints
• tests/customizationController.test.js
javascript
Copy code
// product-customization-service/tests/customizationController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Customization = require('../models/Customization');
const Product = require('../models/Product');
const Template = require('../models/Template');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Customization API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Customization.deleteMany();
await Product.deleteMany();
await Template.deleteMany();
});
describe('POST /api/customizations', () => {
it('should create a new customization', async () => {
// Create a product and template
const product = await Product.create({
name: 'Test Product',
description: 'A product for testing',
price: 99.99,
images: [],
availableOptions: [],
templates: [],
});
const template = await Template.create({
name: 'Test Template',
description: 'A template for testing',
customizableFields: [],
});
const res = await request(app)
.post('/api/customizations')
.set('Authorization', `Bearer ${token}`)
.field('productId', product._id.toString())
.field('templateId', template._id.toString())
.field('selectedOptions', JSON.stringify([{ optionName: 'Color', optionValue: 'Red' }]))
.field('customizedFields', JSON.stringify([{ fieldName: 'Text', fieldValue: 'Hello World' }]))
.attach('image', '__tests__/test-image.png'); // Ensure this test image exists
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Customization created successfully');
expect(res.body.customization).toHaveProperty('product', product._id.toString());
expect(res.body.customization).toHaveProperty('template', template._id.toString());
expect(res.body.customization).toHaveProperty('selectedOptions');
expect(res.body.customization).toHaveProperty('customizedFields');
expect(res.body.customization).toHaveProperty('image');
});
it('should return 404 if product not found', async () => {
const res = await request(app)
.post('/api/customizations')
.set('Authorization', `Bearer ${token}`)
.send({
productId: mongoose.Types.ObjectId(),
selectedOptions: [{ optionName: 'Size', optionValue: 'Large' }],
customizedFields: [{ fieldName: 'Text', fieldValue: 'Test' }],
});
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Product not found');
});
});
// Additional tests for GET, PUT, DELETE endpoints can be implemented similarly
});
• Run Backend Tests:
bash
Copy code
cd product-customization-service
npm test
3.1.10. Dockerize Product Customization Service
1. Create Dockerfile:
dockerfile
Copy code
# product-customization-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Create uploads directory
RUN mkdir uploads
# Expose port
EXPOSE 5008
# Start the service
CMD [ "node", "index.js" ]
2. Update Root docker-compose.yml:
Add the Product Customization Service to your Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
product-customization-service:
build: ./product-customization-service
restart: always
ports:
- "5008:5008"
environment:
- PORT=5008
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
# ... other services
volumes:
mongo-data:
Notes:
o Service Names: Ensure that service names in Docker Compose are consistent and can communicate with each other via these names.
o Environment Variables: The frontend now includes REACT_APP_CUSTOMIZATION_SERVICE_URL pointing to the Product Customization Service.
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend: Accessible at http://localhost:3000/
o Product Customization Service: Accessible at http://localhost:5008/api/customizations
3.1.11. Backend Documentation (README.md)
markdown
Copy code
# Product Customization Service
## Overview
The **Product Customization Service** enables users to personalize products and templates according to their preferences. Users can select options, add text, upload images, and choose styles to create customized products, enhancing their shopping experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- [MongoDB](https://www.mongodb.com/) instance running (local or cloud)
- Existing microservices setup (Auth, Order, Payment, Shipping, AI)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd path/to/ibrandidit-microservices/product-customization-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the product-customization-service/ directory based on the provided .env.example.
env
Copy code
PORT=5008
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Descriptions:
• PORT: Port on which the service runs.
• NODE_ENV: Environment mode (development or production).
• MONGODB_URI: MongoDB connection string.
• JWT_SECRET: Secret key for JWT authentication.
• RABBITMQ_URL: RabbitMQ connection string for message queuing.
Note: Replace your_jwt_secret_key with your actual JWT secret key.
Running the Service
Option 1: Running Locally
1. Start the Service:
bash
Copy code
npm run dev
o The service will run on http://localhost:5008/.
Option 2: Using Docker
1. Build and Start the Service with Docker Compose:
Ensure that the service is included in your docker-compose.yml.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Product Customization Service: Accessible at http://localhost:5008/api/customizations
API Documentation
Authentication
All API endpoints require authentication via JWT. Include the JWT token in the Authorization header as follows:
makefile
Copy code
Authorization: Bearer <token>
Endpoints
1. Create a Customization
• Endpoint: POST /api/customizations
• Description: Creates a new product customization.
• Headers:
o Authorization: Bearer <token>
o Content-Type: multipart/form-data
• Form Data:
o productId (String, required): ID of the product to customize.
o templateId (String, optional): ID of the template to use.
o selectedOptions (JSON, optional): Array of selected options.
o customizedFields (JSON, optional): Array of customized fields.
o image (File, optional): Image file for customization.
• Response:
o 201 Created
o json
Copy code
{
"message": "Customization created successfully",
"customization": { ... }
}
2. Get All Customizations for a User
• Endpoint: GET /api/customizations
• Description: Retrieves all customizations made by the authenticated user.
• Headers:
o Authorization: Bearer <token>
• Response:
o 200 OK
o json
Copy code
{
"customizations": [ ... ]
}
3. Get a Specific Customization
• Endpoint: GET /api/customizations/:id
• Description: Retrieves a specific customization by ID.
• Headers:
o Authorization: Bearer <token>
• Response:
o 200 OK
o json
Copy code
{
"customization": { ... }
}
4. Update a Customization
• Endpoint: PUT /api/customizations/:id
• Description: Updates an existing customization.
• Headers:
o Authorization: Bearer <token>
o Content-Type: multipart/form-data
• Form Data:
o selectedOptions (JSON, optional): Array of updated options.
o customizedFields (JSON, optional): Array of updated fields.
o image (File, optional): New image file for customization.
• Response:
o 200 OK
o json
Copy code
{
"message": "Customization updated successfully",
"customization": { ... }
}
5. Delete a Customization
• Endpoint: DELETE /api/customizations/:id
• Description: Deletes a customization by ID.
• Headers:
o Authorization: Bearer <token>
• Response:
o 200 OK
o json
Copy code
{
"message": "Customization deleted successfully"
}
Error Responses
• 400 Bad Request: Missing or invalid data.
• 401 Unauthorized: Missing or invalid JWT token.
• 404 Not Found: Resource not found.
• 500 Internal Server Error: Server-side errors.
Testing
Backend Testing
1. Run Tests:
bash
Copy code
cd product-customization-service
npm test
o This command runs all tests using Jest and generates coverage reports.
2. Test Coverage:
o After running tests, a coverage/ directory is created containing detailed coverage reports.
Frontend Testing
(To be covered in the frontend section)
Linting and Formatting
Maintain code quality and consistency using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd product-customization-service
npm run lint
2. Run Formatting:
bash
Copy code
cd product-customization-service
npm run format
Deployment
Option 1: Docker Deployment
1. Build the Docker Image:
bash
Copy code
cd product-customization-service
docker build -t product-customization-service .
2. Run the Docker Container:
bash
Copy code
docker run -d -p 5008:5008 --env-file .env product-customization-service
o The service will be accessible at http://localhost:5008/api/customizations.
Option 2: Deploying to Cloud Providers
• Heroku, AWS, Google Cloud, etc.: Follow the respective provider's guidelines to deploy Node.js applications. Ensure environment variables are securely set.
Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/customization-feature
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add product customization endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/customization-feature
o Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
bash
Copy code
#### **3.1.12. Finalizing Backend Code**
Ensure all necessary files are included and properly structured as per the project structure outlined above.
### **3.2. Frontend: Integrate Product & Template Customization Features**
Integrate the Product & Template Customization functionalities into the frontend React application.
#### **3.2.1. Update Environment Variables**
Ensure the frontend `.env` includes the Product Customization Service URL.
```env
REACT_APP_API_BASE_URL=http://order-service:5004
REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
Note: If running locally without Docker, replace service names with localhost and appropriate ports (e.g., http://localhost:5008/api/customizations).
3.2.2. Install Additional Dependencies
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install @mui/icons-material
3.2.3. Create Customization Components
1. Customization Form Component (src/components/Customization/CustomizationForm.js):
javascript
Copy code
// src/components/Customization/CustomizationForm.js
import React, { useState, useEffect } from 'react';
import {
Box,
TextField,
Button,
Typography,
Select,
MenuItem,
InputLabel,
FormControl,
CircularProgress,
Grid,
} from '@mui/material';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const CustomizationForm = () => {
const token = useSelector((state) => state.auth.token);
const [products, setProducts] = useState([]);
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(false);
const API_BASE_URL = process.env.REACT_APP_CUSTOMIZATION_SERVICE_URL;
useEffect(() => {
const fetchProductsAndTemplates = async () => {
try {
const [productRes, templateRes] = await Promise.all([
axios.get(`${API_BASE_URL}`, {
headers: { Authorization: `Bearer ${token}` },
}),
axios.get(`http://product-service:5008/api/templates`, {
headers: { Authorization: `Bearer ${token}` },
}),
]);
setProducts(productRes.data.products);
setTemplates(templateRes.data.templates);
} catch (error) {
toast.error('Failed to fetch products or templates');
}
};
fetchProductsAndTemplates();
}, [API_BASE_URL, token]);
const formik = useFormik({
initialValues: {
productId: '',
templateId: '',
selectedOptions: [],
customizedFields: [],
image: null,
},
validationSchema: Yup.object({
productId: Yup.string().required('Product is required'),
// Additional validations can be added based on customization fields
}),
onSubmit: async (values, { resetForm }) => {
setLoading(true);
try {
const formData = new FormData();
formData.append('productId', values.productId);
if (values.templateId) formData.append('templateId', values.templateId);
formData.append('selectedOptions', JSON.stringify(values.selectedOptions));
formData.append('customizedFields', JSON.stringify(values.customizedFields));
if (values.image) formData.append('image', values.image);
const response = await axios.post(`${API_BASE_URL}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
});
toast.success('Customization created successfully');
resetForm();
} catch (error) {
toast.error('Failed to create customization');
} finally {
setLoading(false);
}
},
});
const handleOptionChange = (index, field, value) => {
const updatedOptions = [...formik.values.selectedOptions];
updatedOptions[index][field] = value;
formik.setFieldValue('selectedOptions', updatedOptions);
};
const handleFieldChange = (index, field, value) => {
const updatedFields = [...formik.values.customizedFields];
updatedFields[index][field] = value;
formik.setFieldValue('customizedFields', updatedFields);
};
const addOption = () => {
formik.setFieldValue('selectedOptions', [
...formik.values.selectedOptions,
{ optionName: '', optionValue: '' },
]);
};
const removeOption = (index) => {
const updatedOptions = formik.values.selectedOptions.filter((_, i) => i !== index);
formik.setFieldValue('selectedOptions', updatedOptions);
};
const addField = () => {
formik.setFieldValue('customizedFields', [
...formik.values.customizedFields,
{ fieldName: '', fieldValue: '' },
]);
};
const removeField = (index) => {
const updatedFields = formik.values.customizedFields.filter((_, i) => i !== index);
formik.setFieldValue('customizedFields', updatedFields);
};
return (
<Box sx={{ mt: 4, p: 2, border: '1px solid #ccc', borderRadius: '8px' }}>
<Typography variant="h6" gutterBottom>
Customize Your Product
</Typography>
<form onSubmit={formik.handleSubmit}>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel id="product-label">Product</InputLabel>
<Select
labelId="product-label"
id="productId"
name="productId"
value={formik.values.productId}
label="Product"
onChange={formik.handleChange}
error={formik.touched.productId && Boolean(formik.errors.productId)}
>
{products.map((product) => (
<MenuItem key={product._id} value={product._id}>
{product.name}
</MenuItem>
))}
</Select>
{formik.touched.productId && formik.errors.productId && (
<Typography color="error" variant="caption">
{formik.errors.productId}
</Typography>
)}
</FormControl>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel id="template-label">Template (Optional)</InputLabel>
<Select
labelId="template-label"
id="templateId"
name="templateId"
value={formik.values.templateId}
label="Template (Optional)"
onChange={formik.handleChange}
>
{templates.map((template) => (
<MenuItem key={template._id} value={template._id}>
{template.name}
</MenuItem>
))}
</Select>
</FormControl>
{/* Selected Options */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle1">Selected Options</Typography>
{formik.values.selectedOptions.map((option, index) => (
<Grid container spacing={2} key={index} sx={{ mb: 1 }}>
<Grid item xs={5}>
<TextField
fullWidth
label="Option Name"
value={option.optionName}
onChange={(e) => handleOptionChange(index, 'optionName', e.target.value)}
/>
</Grid>
<Grid item xs={5}>
<TextField
fullWidth
label="Option Value"
value={option.optionValue}
onChange={(e) => handleOptionChange(index, 'optionValue', e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<Button
variant="contained"
color="secondary"
onClick={() => removeOption(index)}
>
Remove
</Button>
</Grid>
</Grid>
))}
<Button variant="outlined" onClick={addOption}>
Add Option
</Button>
</Box>
{/* Customized Fields */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle1">Customized Fields</Typography>
{formik.values.customizedFields.map((field, index) => (
<Grid container spacing={2} key={index} sx={{ mb: 1 }}>
<Grid item xs={5}>
<TextField
fullWidth
label="Field Name"
value={field.fieldName}
onChange={(e) => handleFieldChange(index, 'fieldName', e.target.value)}
/>
</Grid>
<Grid item xs={5}>
<TextField
fullWidth
label="Field Value"
value={field.fieldValue}
onChange={(e) => handleFieldChange(index, 'fieldValue', e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<Button
variant="contained"
color="secondary"
onClick={() => removeField(index)}
>
Remove
</Button>
</Grid>
</Grid>
))}
<Button variant="outlined" onClick={addField}>
Add Field
</Button>
</Box>
{/* Image Upload */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle1">Upload Custom Image (Optional)</Typography>
<input
id="image"
name="image"
type="file"
accept="image/*"
onChange={(event) => {
formik.setFieldValue('image', event.currentTarget.files[0]);
}}
/>
{formik.errors.image && (
<Typography color="error" variant="caption">
{formik.errors.image}
</Typography>
)}
</Box>
<Button color="primary" variant="contained" fullWidth type="submit" disabled={loading}>
{loading ? <CircularProgress size={24} /> : 'Create Customization'}
</Button>
</form>
</Box>
);
};
export default CustomizationForm;
2. Display Customizations Component (src/components/Customization/CustomizationList.js):
javascript
Copy code
// src/components/Customization/CustomizationList.js
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
Paper,
Grid,
Button,
CircularProgress,
Card,
CardMedia,
CardContent,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const CustomizationList = () => {
const token = useSelector((state) => state.auth.token);
const [customizations, setCustomizations] = useState([]);
const [loading, setLoading] = useState(true);
const API_BASE_URL = process.env.REACT_APP_CUSTOMIZATION_SERVICE_URL;
useEffect(() => {
const fetchCustomizations = async () => {
try {
const response = await axios.get(`${API_BASE_URL}`, {
headers: { Authorization: `Bearer ${token}` },
});
setCustomizations(response.data.customizations);
} catch (error) {
toast.error('Failed to fetch customizations');
} finally {
setLoading(false);
}
};
fetchCustomizations();
}, [API_BASE_URL, token]);
const handleDelete = async (id) => {
try {
await axios.delete(`${API_BASE_URL}/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
setCustomizations(customizations.filter((c) => c._id !== id));
toast.success('Customization deleted successfully');
} catch (error) {
toast.error('Failed to delete customization');
}
};
if (loading) {
return <CircularProgress />;
}
return (
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
Your Customizations
</Typography>
{customizations.length === 0 ? (
<Typography>No customizations found.</Typography>
) : (
<Grid container spacing={2}>
{customizations.map((customization) => (
<Grid item xs={12} md={6} key={customization._id}>
<Card>
{customization.image && (
<CardMedia
component="img"
height="200"
image={customization.image}
alt="Customized Product"
/>
)}
<CardContent>
<Typography variant="h6">{customization.product.name}</Typography>
<Typography variant="body2" color="text.secondary">
{customization.product.description}
</Typography>
<Typography variant="body1" sx={{ mt: 1 }}>
Price: ${customization.product.price.toFixed(2)}
</Typography>
<Typography variant="subtitle1" sx={{ mt: 1 }}>
Selected Options:
</Typography>
<ul>
{customization.selectedOptions.map((option, index) => (
<li key={index}>
{option.optionName}: {option.optionValue}
</li>
))}
</ul>
<Typography variant="subtitle1">Customized Fields:</Typography>
<ul>
{customization.customizedFields.map((field, index) => (
<li key={index}>
{field.fieldName}: {field.fieldValue}
</li>
))}
</ul>
<Button
variant="contained"
color="secondary"
onClick={() => handleDelete(customization._id)}
>
Delete
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
);
};
export default CustomizationList;
3. Templates List Component (src/components/Customization/TemplateList.js):
(Assuming there is an endpoint to fetch templates; otherwise, integrate accordingly)
javascript
Copy code
// src/components/Customization/TemplateList.js
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
Paper,
Grid,
CircularProgress,
Card,
CardMedia,
CardContent,
Button,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const TemplateList = () => {
const token = useSelector((state) => state.auth.token);
const [templates, setTemplates] = useState([]);
const [loading, setLoading] = useState(true);
const TEMPLATE_API_URL = 'http://product-customization-service:5008/api/templates';
useEffect(() => {
const fetchTemplates = async () => {
try {
const response = await axios.get(TEMPLATE_API_URL, {
headers: { Authorization: `Bearer ${token}` },
});
setTemplates(response.data.templates);
} catch (error) {
toast.error('Failed to fetch templates');
} finally {
setLoading(false);
}
};
fetchTemplates();
}, [TEMPLATE_API_URL, token]);
if (loading) {
return <CircularProgress />;
}
return (
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
Available Templates
</Typography>
{templates.length === 0 ? (
<Typography>No templates available.</Typography>
) : (
<Grid container spacing={2}>
{templates.map((template) => (
<Grid item xs={12} md={4} key={template._id}>
<Card>
{/* Assuming templates have an image; if not, remove CardMedia */}
{template.image && (
<CardMedia
component="img"
height="140"
image={template.image}
alt={template.name}
/>
)}
<CardContent>
<Typography variant="h6">{template.name}</Typography>
<Typography variant="body2" color="text.secondary">
{template.description}
</Typography>
<Button variant="outlined" sx={{ mt: 1 }}>
Use Template
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
);
};
export default TemplateList;
4. Integrate Customization Components into Dashboard (src/pages/Dashboard/Dashboard.js):
javascript
Copy code
// src/pages/Dashboard/Dashboard.js
import React from 'react';
import { Typography, Box, Grid, Paper } from '@mui/material';
import { useSelector } from 'react-redux';
import Chatbot from '../../components/Chatbot/Chatbot';
import ImageGenerator from '../../components/ImageGenerator/ImageGenerator';
import CustomizationForm from '../../components/Customization/CustomizationForm';
import CustomizationList from '../../components/Customization/CustomizationList';
import TemplateList from '../../components/Customization/TemplateList';
const Dashboard = () => {
const user = useSelector((state) => state.auth.user);
return (
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
Welcome, {user.name}!
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<CustomizationForm />
</Paper>
</Grid>
<Grid item xs={12} md={8}>
<Paper sx={{ padding: 2 }}>
<CustomizationList />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<TemplateList />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<Chatbot />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<ImageGenerator />
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default Dashboard;
3.2.4. Update Redux Store (if necessary)
Since the customization functionalities are handled independently, no specific Redux state management is required beyond existing configurations. However, ensure that Axios base URLs are correctly set up if needed.
3.2.5. Testing Frontend Components
1. Customization Form Component Test (src/components/Customization/CustomizationForm.test.js):
javascript
Copy code
// src/components/Customization/CustomizationForm.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import CustomizationForm from './CustomizationForm';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('CustomizationForm Component', () => {
beforeEach(() => {
axios.get.mockResolvedValue({
data: {
products: [
{ _id: 'prod1', name: 'Product 1' },
{ _id: 'prod2', name: 'Product 2' },
],
},
});
axios.get.mockResolvedValueOnce({
data: {
templates: [
{ _id: 'temp1', name: 'Template 1' },
{ _id: 'temp2', name: 'Template 2' },
],
},
});
});
test('renders customization form', async () => {
render(
<Provider store={store}>
<CustomizationForm />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Customize Your Product/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByLabelText(/Product/i)).toBeInTheDocument();
});
});
test('submits form with valid data', async () => {
axios.post.mockResolvedValue({
data: { message: 'Customization created successfully' },
});
render(
<Provider store={store}>
<CustomizationForm />
<ToastContainer />
</Provider>
);
// Wait for products and templates to load
await waitFor(() => {
expect(screen.getByLabelText(/Product/i)).toBeInTheDocument();
});
// Select Product
fireEvent.mouseDown(screen.getByLabelText(/Product/i));
const productOption = await screen.findByText('Product 1');
fireEvent.click(productOption);
// Add an option
fireEvent.click(screen.getByText(/Add Option/i));
const optionNameInput = screen.getByLabelText('Option Name');
const optionValueInput = screen.getByLabelText('Option Value');
fireEvent.change(optionNameInput, { target: { value: 'Color' } });
fireEvent.change(optionValueInput, { target: { value: 'Red' } });
// Add a field
fireEvent.click(screen.getByText(/Add Field/i));
const fieldNameInput = screen.getByLabelText('Field Name');
const fieldValueInput = screen.getByLabelText('Field Value');
fireEvent.change(fieldNameInput, { target: { value: 'Text' } });
fireEvent.change(fieldValueInput, { target: { value: 'Hello World' } });
// Submit the form
fireEvent.click(screen.getByRole('button', { name: /Create Customization/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalled();
expect(screen.getByText(/Customization created successfully/i)).toBeInTheDocument();
});
});
test('shows error on failed submission', async () => {
axios.post.mockRejectedValue(new Error('Failed to create customization'));
render(
<Provider store={store}>
<CustomizationForm />
<ToastContainer />
</Provider>
);
// Wait for products and templates to load
await waitFor(() => {
expect(screen.getByLabelText(/Product/i)).toBeInTheDocument();
});
// Select Product
fireEvent.mouseDown(screen.getByLabelText(/Product/i));
const productOption = await screen.findByText('Product 1');
fireEvent.click(productOption);
// Submit the form without options
fireEvent.click(screen.getByRole('button', { name: /Create Customization/i }));
await waitFor(() => {
expect(screen.getByText(/Customization created successfully/i)).not.toBeInTheDocument();
expect(screen.getByText(/Failed to create customization/i)).toBeInTheDocument();
});
});
});
2. Customization List Component Test (src/components/Customization/CustomizationList.test.js):
javascript
Copy code
// src/components/Customization/CustomizationList.test.js
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import CustomizationList from './CustomizationList';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('CustomizationList Component', () => {
beforeEach(() => {
axios.get.mockResolvedValue({
data: {
customizations: [
{
_id: 'cust1',
product: { name: 'Product 1', description: 'Desc 1', price: 100 },
selectedOptions: [{ optionName: 'Color', optionValue: 'Red' }],
customizedFields: [{ fieldName: 'Text', fieldValue: 'Hello' }],
image: '/uploads/image1.png',
},
],
},
});
});
test('renders customization list', async () => {
render(
<Provider store={store}>
<CustomizationList />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Your Customizations/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Product 1/i)).toBeInTheDocument();
});
});
test('deletes a customization', async () => {
axios.delete.mockResolvedValue({
data: { message: 'Customization deleted successfully' },
});
render(
<Provider store={store}>
<CustomizationList />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Product 1/i)).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Delete/i }));
await waitFor(() => {
expect(axios.delete).toHaveBeenCalled();
expect(screen.queryByText(/Product 1/i)).not.toBeInTheDocument();
expect(screen.getByText(/Customization deleted successfully/i)).toBeInTheDocument();
});
});
test('shows error on failed fetch', async () => {
axios.get.mockRejectedValue(new Error('Failed to fetch customizations'));
render(
<Provider store={store}>
<CustomizationList />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Failed to fetch customizations/i)).toBeInTheDocument();
});
});
});
3. Run Frontend Tests:
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm test
o Ensure all tests pass, especially those related to customization functionalities.
3.2.6. Frontend Documentation (README.md)
Update the README.md in the frontend/user-dashboard directory to include information about the Product & Template Customization features.
markdown
Copy code
# User Dashboard
## Overview
The **User Dashboard** is a React.js frontend application that provides users with a centralized platform to manage their profiles, view order history, track shipments, manage payments, interact with AI-powered features like chatbots and image generation, customize products and templates, subscribe to newsletters, and engage with social media. This dashboard integrates seamlessly with backend microservices, ensuring a smooth and secure user experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Application](#running-the-application)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Backend Microservices running (Auth, Order, Payment, Shipping, AI, Product Customization)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Frontend Directory:**
```bash
cd path/to/ibrandidit-microservices/frontend/user-dashboard
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-dashboard/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://order-service:5004
REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
Descriptions:
• REACT_APP_API_BASE_URL: Base URL for the Order Service.
• REACT_APP_SHIPPING_SERVICE_URL: Base URL for the Shipping Service.
• REACT_APP_PAYMENT_SERVICE_URL: Base URL for the Payment Service.
• REACT_APP_AUTH_SERVICE_URL: Base URL for the Auth Service.
• REACT_APP_GOOGLE_ANALYTICS_ID: Your Google Analytics Tracking ID.
• REACT_APP_AI_SERVICE_URL: Base URL for the AI Service.
• REACT_APP_CUSTOMIZATION_SERVICE_URL: Base URL for the Product Customization Service.
Note: Replace the URLs with actual service endpoints if using Docker Compose with service names (e.g., http://localhost:5008/api/customizations).
Running the Application
Option 1: Running Locally
1. Start Backend Microservices:
Ensure that all backend services (Auth, Order, Payment, Shipping, AI, Product Customization) are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
2. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
o The application will run on http://localhost:3000/.
o Login/Register to access the dashboard.
Option 2: Using Docker
1. Ensure Docker Compose is Updated:
Verify that docker-compose.yml includes the Product Customization Service and that all environment variables are correctly set.
2. Start All Services with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend accessible at http://localhost:3000/.
API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Auth Service: http://localhost:5003/api-docs
• Order Service: http://localhost:5004/api-docs
• Payment Service: http://localhost:5005/api-docs
• Shipping Service: http://localhost:5006/api-docs
• AI Service: http://localhost:5007/api-docs
• Product Customization Service: http://localhost:5008/api-docs
Testing
Testing ensures that the Product & Template Customization integrations work as expected.
Backend Testing
1. Run Product Customization Service Tests:
bash
Copy code
cd product-customization-service
npm test
2. Expected Results:
o All tests should pass, ensuring that customization functionalities work as intended.
Frontend Testing
1. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Ensure all tests pass, especially those related to customization functionalities.
Linting and Formatting
Maintain code quality using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd frontend/user-dashboard
npm run lint
2. Run Formatting:
bash
Copy code
cd frontend/user-dashboard
npm run format
Deployment
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
o This command creates a production-ready build in the build/ directory.
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend will be accessible at http://localhost:3000/.
o Ensure that all environment variables are correctly set in the .env files.
o Replace placeholder values with actual credentials before deploying to production.
Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/customization-feature
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add product customization form"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/customization-feature
o Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
php
Copy code
#### **3.2.7. Frontend Integration with Backend**
1. **Fetch Products and Templates:**
Ensure that the `CustomizationForm` fetches products and templates correctly from the respective services.
2. **Handle Image Uploads:**
The form allows users to upload images as part of their customization. Ensure that the backend handles multipart/form-data correctly and that images are stored securely.
3. **Display Customizations:**
The `CustomizationList` component displays all customizations made by the user, fetching data from the Product Customization Service.
4. **Secure API Calls:**
All API requests to the Product Customization Service must include the JWT token in the `Authorization` header.
5. **Error Handling and Notifications:**
Use `React Toastify` to display success and error messages to users based on API responses.
#### **3.2.8. Update Frontend API Service (`src/services/api.js`)**
If you have a centralized API service, ensure it includes endpoints for Product Customization.
```javascript
// src/services/api.js
import axios from 'axios';
import store from '../redux/store';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const CUSTOMIZATION_SERVICE_URL = process.env.REACT_APP_CUSTOMIZATION_SERVICE_URL;
const api = axios.create({
baseURL: API_BASE_URL,
});
// Request interceptor to add JWT token
api.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Export APIs for different services
export const customizationApi = axios.create({
baseURL: CUSTOMIZATION_SERVICE_URL,
headers: {
'Content-Type': 'application/json',
},
});
customizationApi.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default api;
3.2.9. Finalizing Frontend Code
Ensure that all components are correctly imported and integrated into the dashboard. Also, make sure that navigation routes (if any) are updated accordingly.
3.3. Testing
Testing is crucial to ensure that both backend and frontend integrations work seamlessly.
3.3.1. Backend Testing
1. Run Backend Tests:
bash
Copy code
cd product-customization-service
npm test
o Ensure all tests pass, verifying the functionality of customization endpoints.
2. Manual Testing:
o Use tools like Postman or Insomnia to manually test API endpoints.
o Test creating, fetching, updating, and deleting customizations.
o Verify that image uploads work correctly and that images are stored in the uploads/ directory.
3.3.2. Frontend Testing
1. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Ensure that all tests pass, particularly those related to the customization form and list components.
2. Manual Testing:
o Customization Form:
Navigate to the dashboard and access the customization form.
Select a product and optionally a template.
Add options and customized fields.
Upload an image.
Submit the form and verify that the customization is created.
o Customization List:
View the list of customizations.
Verify that the customization details are accurate.
Delete a customization and confirm its removal.
o Error Handling:
Attempt to submit incomplete forms and verify that validation errors are displayed.
Test with invalid image formats and sizes to ensure that the system handles them gracefully.
3.4. Documentation
Ensure that all new features are well-documented for easy integration and maintenance.
3.4.1. Create README.md
Update the README.md in the product-customization-service and frontend/user-dashboard directories to include information about the Product & Template Customization features.
(Already provided in the backend and frontend documentation sections above)
3.5. Additional Configuration and Best Practices
1. Rate Limiting:
Implement rate limiting on the Product Customization Service endpoints to prevent abuse and manage API usage efficiently.
o Install express-rate-limit:
bash
Copy code
npm install express-rate-limit
o Update index.js:
javascript
Copy code
// product-customization-service/index.js
const rateLimit = require('express-rate-limit');
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
2. Caching:
Implement caching mechanisms for frequently accessed data, such as product and template lists, to reduce database load and improve response times.
o Install node-cache:
bash
Copy code
npm install node-cache
o Update Controllers:
Use caching in the getUserCustomizations endpoint to cache responses.
3. Logging:
Implement logging to monitor service performance and debug issues.
o Install winston:
bash
Copy code
npm install winston
o Set Up Logger (utils/logger.js):
javascript
Copy code
// product-customization-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'product-customization-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
o Use Logger in index.js and Controllers:
javascript
Copy code
// product-customization-service/index.js
const logger = require('./utils/logger');
// Replace console.log with logger.info
logger.info('MongoDB Connected');
logger.info(`Product Customization Service running on port ${PORT}`);
4. Security:
o Input Validation: Use libraries like express-validator to validate and sanitize user inputs.
o Helmet: Install and use helmet to set secure HTTP headers.
bash
Copy code
npm install helmet
javascript
Copy code
// product-customization-service/index.js
const helmet = require('helmet');
app.use(helmet());
5. File Upload Security:
o Limit File Types and Sizes: Already handled using Multer's fileFilter and limits.
o Store Files Securely: Serve uploaded files from a secure directory and consider using cloud storage services like AWS S3 for production.
6. Responsive Design:
Ensure that customization forms and lists are responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
7. Error Handling:
Implement comprehensive error handling to provide meaningful feedback to users and maintain application stability.
o Use centralized error handling middleware.
o Display user-friendly error messages in the frontend.
8. Version Control:
o Commit changes regularly with clear messages.
o Use feature branches for new functionalities.
o Merge changes via pull requests with code reviews.
________________________________________
4. Complete Code and Configuration Files
4.1. Backend: Product Customization Service
1. product-customization-service/package.json
json
Copy code
{
"name": "product-customization-service",
"version": "1.0.0",
"description": "Product Customization Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"multer": "^1.4.5-lts.1",
"express-rate-limit": "^6.7.0",
"helmet": "^6.1.6",
"node-cache": "^5.1.2",
"winston": "^3.8.2"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
}
}
2. product-customization-service/index.js
javascript
Copy code
// product-customization-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const customizationRoutes = require('./routes/customizationRoutes');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Static folder for uploads
app.use('/uploads', express.static('uploads'));
// Routes
app.use('/api/customizations', customizationRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Product Customization Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5008;
app.listen(PORT, () => {
logger.info(`Product Customization Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
3. product-customization-service/utils/logger.js
javascript
Copy code
// product-customization-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'product-customization-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
4. product-customization-service/utils/upload.js
(Already provided above)
5. product-customization-service/models/Product.js
(Already provided above)
6. product-customization-service/models/Template.js
(Already provided above)
7. product-customization-service/models/Customization.js
(Already provided above)
8. product-customization-service/controllers/customizationController.js
(Already provided above)
9. product-customization-service/routes/customizationRoutes.js
(Already provided above)
10. product-customization-service/middleware/authenticate.js
(Already provided above)
11. product-customization-service/tests/customizationController.test.js
(Already provided above)
12. product-customization-service/Dockerfile
(Already provided above)
4.2. Frontend: User Dashboard
1. frontend/user-dashboard/package.json
(Already provided in previous stages)
2. frontend/user-dashboard/src/redux/slices/authSlice.js
(Already provided in previous stages)
3. frontend/user-dashboard/src/redux/slices/orderSlice.js
(Already provided in previous stages)
4. frontend/user-dashboard/src/redux/slices/shipmentSlice.js
(Already provided in previous stages)
5. frontend/user-dashboard/src/redux/slices/paymentSlice.js
(Already provided in previous stages)
6. frontend/user-dashboard/src/redux/store.js
(Already provided in previous stages)
7. frontend/user-dashboard/src/services/api.js
(Already provided above)
8. frontend/user-dashboard/src/components/Layout/Layout.js
(Already provided in previous stages)
9. frontend/user-dashboard/src/components/PrivateRoute/PrivateRoute.js
(Already provided in previous stages)
10. frontend/user-dashboard/src/components/Dashboard/Orders.js
(Already provided in previous stages)
11. frontend/user-dashboard/src/components/Dashboard/Shipments.js
(Already provided in previous stages)
12. frontend/user-dashboard/src/components/Dashboard/Payments.js
(Already provided in previous stages)
13. frontend/user-dashboard/src/components/SocialShare/SocialShare.js
(Already provided in previous stages)
14. frontend/user-dashboard/src/components/Newsletter/Newsletter.js
(Already provided in previous stages)
15. frontend/user-dashboard/src/pages/AuthCallback/AuthCallback.js
(Already provided in previous stages)
16. frontend/user-dashboard/src/pages/Home/Home.js
(Already provided in previous stages)
17. frontend/user-dashboard/src/pages/Login/Login.js
(Already provided in previous stages)
18. frontend/user-dashboard/src/pages/Register/Register.js
(Already provided in previous stages)
19. frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided above)
20. frontend/user-dashboard/src/pages/Profile/Profile.js
(Already provided in previous stages)
21. frontend/user-dashboard/src/pages/ProductDetail/ProductDetail.js
(Already provided in previous stages)
22. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided in previous stages)
23. frontend/user-dashboard/src/App.js
(Already provided above)
24. frontend/user-dashboard/src/index.js
(Already provided above)
25. frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
26. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
27. frontend/user-dashboard/src/components/Customization/CustomizationForm.js
(Already provided above)
28. frontend/user-dashboard/src/components/Customization/CustomizationList.js
(Already provided above)
29. frontend/user-dashboard/src/components/Customization/TemplateList.js
(Already provided above)
30. frontend/user-dashboard/src/components/Customization/CustomizationForm.test.js
(Already provided above)
31. frontend/user-dashboard/src/components/Customization/CustomizationList.test.js
(Already provided above)
4.3. Docker Configuration
1. ibrandidit-microservices/docker-compose.yml
(Already provided above)
2. Build and Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend: Accessible at http://localhost:3000/
o Product Customization Service: Accessible at http://localhost:5008/api/customizations
4.4. Frontend Components
1. frontend/user-dashboard/src/components/Customization/CustomizationForm.js
(Already provided above)
2. frontend/user-dashboard/src/components/Customization/CustomizationList.js
(Already provided above)
3. frontend/user-dashboard/src/components/Customization/TemplateList.js
(Already provided above)
4. frontend/user-dashboard/src/pages/Dashboard/Dashboard.js
(Already provided above)
5. frontend/user-dashboard/src/App.js
(Already provided above)
6. frontend/user-dashboard/src/index.js
(Already provided above)
7. frontend/user-dashboard/src/components/RouteChangeTracker.js
(Already provided above)
8. frontend/user-dashboard/src/components/Chatbot/Chatbot.js
(Already provided above)
9. frontend/user-dashboard/src/components/ImageGenerator/ImageGenerator.js
(Already provided above)
10. frontend/user-dashboard/src/components/Customization/CustomizationForm.test.js
(Already provided above)
11. frontend/user-dashboard/src/components/Customization/CustomizationList.test.js
(Already provided above)
4.5. Frontend: Customization Service Integration
1. Fetch Products and Templates:
Ensure that the CustomizationForm component fetches products from the Order Service and templates from the Product Customization Service.
o Modify CustomizationForm.js:
javascript
Copy code
// src/components/Customization/CustomizationForm.js
// Update fetch URLs
const fetchProductsAndTemplates = async () => {
try {
const [productRes, templateRes] = await Promise.all([
axios.get(`http://order-service:5004/api/products`, {
headers: { Authorization: `Bearer ${token}` },
}),
axios.get(`http://product-customization-service:5008/api/templates`, {
headers: { Authorization: `Bearer ${token}` },
}),
]);
setProducts(productRes.data.products);
setTemplates(templateRes.data.templates);
} catch (error) {
toast.error('Failed to fetch products or templates');
}
};
o Ensure CORS is Configured Properly on both services to allow requests from the frontend origin.
2. Handle Image Uploads:
The form allows users to upload images as part of their customization. Ensure that:
o The backend handles multipart/form-data correctly using Multer.
o The frontend sends the image as part of FormData.
o The uploaded image is accessible via the /uploads route.
3. Display Customizations:
The CustomizationList component fetches and displays all customizations made by the user.
o Ensure that the backend populates the product and template fields to provide detailed information.
4. Secure API Calls:
All API requests to the Product Customization Service must include the JWT token in the Authorization header.
o Ensure Axios is configured with interceptors to include the token.
5. Error Handling and Notifications:
Use React Toastify to display success and error messages based on API responses.
o Example:
javascript
Copy code
toast.success('Customization created successfully');
toast.error('Failed to create customization');
6. Responsive Design:
Ensure that the customization forms and lists are responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
________________________________________
5. Troubleshooting Tips
1. Backend Service Not Running:
o Problem: Product Customization Service endpoints are unresponsive.
o Solution:
Ensure that the service is running by checking Docker containers.
Review service logs for any errors.
Verify that MongoDB is connected successfully.
2. CORS Errors:
o Problem: Frontend cannot communicate with the Product Customization Service due to CORS policy.
o Solution:
Ensure that the Product Customization Service has CORS enabled (app.use(cors())).
Verify that the frontend origin is allowed in the CORS configuration.
3. Image Upload Failures:
o Problem: Users cannot upload images during customization.
o Solution:
Check that the frontend form correctly attaches the image file.
Ensure that the backend Multer configuration allows image uploads and handles them correctly.
Verify that the uploads/ directory exists and has proper permissions.
4. Authentication Errors:
o Problem: Frontend receives 401 Unauthorized errors when accessing customization endpoints.
o Solution:
Ensure that JWT tokens are correctly stored and included in API requests.
Verify that the JWT_SECRET is consistent across services.
Check that the authentication middleware is correctly implemented in the backend.
5. Data Fetching Issues:
o Problem: Products or templates are not loading in the customization form.
o Solution:
Ensure that the respective backend services (Order Service and Product Customization Service) are running and accessible.
Check API response structures to ensure that data is being sent correctly.
Review frontend API calls for correct URLs and headers.
6. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
7. Form Validation Errors:
o Problem: Users cannot submit customization forms due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
8. Deployment Challenges:
o Problem: Customization features work locally but fail after deployment.
o Solution:
Ensure that environment variables are correctly set in the production environment.
Verify that backend services are accessible from the frontend in the production setup.
Check for any build or runtime errors during deployment.
________________________________________
6. Additional Resources
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Redux Documentation: https://redux.js.org/introduction/getting-started
• Material-UI Documentation: https://mui.com/getting-started/usage/
• Formik Documentation: https://formik.org/docs/overview
• Yup Documentation: https://github.com/jquense/yup
• Axios Documentation: https://axios-http.com/docs/intro
• React Toastify Documentation: https://fkhadra.github.io/react-toastify/introduction
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Passport.js Documentation: http://www.passportjs.org/docs/
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 16: Product & Template Customization within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced customization features, enhancing security measures, or optimizing performance.
________________________________________
By following this comprehensive guide for Stage 16: Product & Template Customization, you've successfully empowered your eCommerce platform with robust customization capabilities. These features not only enhance user interaction and personalization but also contribute to increased sales and customer loyalty.
Next Steps:
Proceed to Stage 17: Advanced Analytics Integration to gain deeper insights into user behavior and platform performance, enabling data-driven decision-making.
Stage 17: Affiliate & Distributor Management is a critical phase that facilitates the growth and reach of your eCommerce platform by leveraging partnerships. This stage enables the management of affiliates and distributors, tracking their performance, handling commissions, and fostering collaborative marketing efforts. Below is a comprehensive guide to implementing this stage, complete with code, dependencies, configuration files, and documentation suitable for beginners.
________________________________________
Stage 17: Affiliate & Distributor Management
________________________________________
1. Overview
Objective:
• Affiliate Management:
o Allow users to register as affiliates.
o Provide affiliates with unique referral links or codes.
o Track referrals and associated commissions.
o Enable affiliates to view their performance and earnings.
• Distributor Management:
o Manage distributors responsible for specific regions or products.
o Assign products or regions to distributors.
o Track distributor performance and sales.
o Handle distributor commissions and payouts.
Benefits:
• Extended Reach: Utilize affiliates and distributors to reach a broader audience.
• Performance-Based Marketing: Reward partners based on their contribution to sales.
• Scalability: Easily scale marketing efforts without significant upfront costs.
• Enhanced Tracking: Monitor the effectiveness of different affiliates and distributors.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js – For building scalable API endpoints.
• Frontend Framework: React.js – For creating interactive user interfaces.
• State Management: Redux – For managing application state.
• Database: MongoDB – For storing affiliate and distributor data.
• Object Data Modeling (ODM): Mongoose – For modeling and interacting with MongoDB.
• Authentication: JWT (JSON Web Tokens) – For securing API endpoints.
• File Storage: Multer – For handling file uploads (if necessary).
• UI Library: Material-UI (MUI) – For pre-built UI components and styling.
• Form Handling: Formik and Yup – For form management and validation.
• Notifications: React Toastify – For displaying notifications to users.
• Testing: Jest and React Testing Library – For unit and integration testing.
• Version Control: Git – Continuation from previous stages.
• Documentation: Markdown and Swagger – For API documentation.
________________________________________
3. Implementation Steps
3.1. Backend: Create Affiliate & Distributor Management Service
To manage affiliates and distributors, we'll create a dedicated Affiliate & Distributor Management Service.
3.1.1. Set Up the Affiliate & Distributor Management Service
1. Create the Service Directory:
bash
Copy code
mkdir affiliate-distributor-service
cd affiliate-distributor-service
npm init -y
2. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors multer
npm install --save-dev nodemon eslint prettier jest supertest
3. Project Structure:
Organize the service as follows:
arduino
Copy code
affiliate-distributor-service/
├── controllers/
│ ├── affiliateController.js
│ ├── distributorController.js
├── models/
│ ├── Affiliate.js
│ ├── Distributor.js
│ ├── Referral.js
│ ├── Commission.js
├── routes/
│ ├── affiliateRoutes.js
│ ├── distributorRoutes.js
├── uploads/
│ ├── (Uploaded files will be stored here, if any)
├── utils/
│ ├── upload.js
├── middleware/
│ ├── authenticate.js
├── tests/
│ ├── affiliateController.test.js
│ ├── distributorController.test.js
├── .env
├── .gitignore
├── index.js
├── package.json
├── README.md
└── ...
3.1.2. Configuration Files
• .gitignore:
gitignore
Copy code
# dependencies
/node_modules
# production
/build
# uploads
/uploads
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
• .env:
env
Copy code
PORT=5009
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/affiliatedb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Note: Replace your_jwt_secret_key with your actual JWT secret key.
• package.json:
json
Copy code
{
"name": "affiliate-distributor-service",
"version": "1.0.0",
"description": "Affiliate & Distributor Management Service for iBranDIDit.com",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"format": "prettier --write .",
"docs": "jsdoc -c jsdoc.json"
},
"keywords": [],
"author": "Your Name",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongoose": "^7.0.3",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"eslint": "^8.50.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.28.1",
"jest": "^29.6.1",
"nodemon": "^3.0.1",
"prettier": "^2.8.8",
"supertest": "^6.3.3"
}
}
3.1.3. Define Data Models
1. Affiliate Model (models/Affiliate.js):
javascript
Copy code
// affiliate-distributor-service/models/Affiliate.js
const mongoose = require('mongoose');
const AffiliateSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
referralCode: {
type: String,
unique: true,
required: true,
},
commissions: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Commission',
},
],
referrals: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Referral',
},
],
status: {
type: String,
enum: ['active', 'inactive'],
default: 'active',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Affiliate', AffiliateSchema);
2. Distributor Model (models/Distributor.js):
javascript
Copy code
// affiliate-distributor-service/models/Distributor.js
const mongoose = require('mongoose');
const DistributorSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
region: {
type: String,
required: true,
},
assignedProducts: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
},
],
sales: {
type: Number,
default: 0,
},
commissions: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Commission',
},
],
status: {
type: String,
enum: ['active', 'inactive'],
default: 'active',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Distributor', DistributorSchema);
3. Referral Model (models/Referral.js):
javascript
Copy code
// affiliate-distributor-service/models/Referral.js
const mongoose = require('mongoose');
const ReferralSchema = new mongoose.Schema(
{
affiliate: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Affiliate',
required: true,
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
order: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Order',
},
status: {
type: String,
enum: ['pending', 'completed'],
default: 'pending',
},
commission: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Commission',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Referral', ReferralSchema);
4. Commission Model (models/Commission.js):
javascript
Copy code
// affiliate-distributor-service/models/Commission.js
const mongoose = require('mongoose');
const CommissionSchema = new mongoose.Schema(
{
affiliate: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Affiliate',
},
distributor: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Distributor',
},
amount: {
type: Number,
required: true,
},
status: {
type: String,
enum: ['pending', 'paid'],
default: 'pending',
},
type: {
type: String,
enum: ['affiliate', 'distributor'],
required: true,
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Commission', CommissionSchema);
3.1.4. Set Up Express Server (index.js)
javascript
Copy code
// affiliate-distributor-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const affiliateRoutes = require('./routes/affiliateRoutes');
const distributorRoutes = require('./routes/distributorRoutes');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Static folder for uploads (if needed)
app.use('/uploads', express.static('uploads'));
// Routes
app.use('/api/affiliates', affiliateRoutes);
app.use('/api/distributors', distributorRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Affiliate & Distributor Management Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5009;
app.listen(PORT, () => {
logger.info(`Affiliate & Distributor Management Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
3.1.5. File Upload Handling (utils/upload.js)
(Optional, if handling file uploads)
javascript
Copy code
// affiliate-distributor-service/utils/upload.js
const multer = require('multer');
const path = require('path');
// Storage configuration
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // Uploads directory
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix + path.extname(file.originalname)); // Unique filename
},
});
// File filter for images
const fileFilter = (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (extname && mimetype) {
return cb(null, true);
}
cb(new Error('Only images are allowed'));
};
// Initialize upload
const upload = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter,
});
module.exports = upload;
3.1.6. Customization Controllers
1. Affiliate Controller (controllers/affiliateController.js):
javascript
Copy code
// affiliate-distributor-service/controllers/affiliateController.js
const Affiliate = require('../models/Affiliate');
const Referral = require('../models/Referral');
const Commission = require('../models/Commission');
const crypto = require('crypto');
// Generate unique referral code
const generateReferralCode = () => {
return crypto.randomBytes(4).toString('hex');
};
// Register as Affiliate
const registerAffiliate = async (req, res) => {
const userId = req.user.id;
try {
// Check if user is already an affiliate
const existingAffiliate = await Affiliate.findOne({ user: userId });
if (existingAffiliate) {
return res.status(400).json({ message: 'User is already an affiliate' });
}
// Generate referral code
const referralCode = generateReferralCode();
const affiliate = new Affiliate({
user: userId,
referralCode,
});
await affiliate.save();
res.status(201).json({ message: 'Affiliate registered successfully', affiliate });
} catch (error) {
console.error('Error registering affiliate:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get Affiliate Details
const getAffiliateDetails = async (req, res) => {
const userId = req.user.id;
try {
const affiliate = await Affiliate.findOne({ user: userId })
.populate('referrals')
.populate('commissions');
if (!affiliate) {
return res.status(404).json({ message: 'Affiliate not found' });
}
res.status(200).json({ affiliate });
} catch (error) {
console.error('Error fetching affiliate details:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Create a Referral
const createReferral = async (req, res) => {
const { referralCode, userId } = req.body; // userId is the referred user
try {
const affiliate = await Affiliate.findOne({ referralCode });
if (!affiliate) {
return res.status(404).json({ message: 'Invalid referral code' });
}
// Check if the referred user already has a referral
const existingReferral = await Referral.findOne({ user: userId });
if (existingReferral) {
return res.status(400).json({ message: 'User has already been referred' });
}
const referral = new Referral({
affiliate: affiliate._id,
user: userId,
});
await referral.save();
affiliate.referrals.push(referral._id);
await affiliate.save();
res.status(201).json({ message: 'Referral created successfully', referral });
} catch (error) {
console.error('Error creating referral:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
registerAffiliate,
getAffiliateDetails,
createReferral,
};
2. Distributor Controller (controllers/distributorController.js):
javascript
Copy code
// affiliate-distributor-service/controllers/distributorController.js
const Distributor = require('../models/Distributor');
const Commission = require('../models/Commission');
// Register as Distributor
const registerDistributor = async (req, res) => {
const userId = req.user.id;
const { region } = req.body;
try {
// Check if user is already a distributor
const existingDistributor = await Distributor.findOne({ user: userId });
if (existingDistributor) {
return res.status(400).json({ message: 'User is already a distributor' });
}
const distributor = new Distributor({
user: userId,
region,
});
await distributor.save();
res.status(201).json({ message: 'Distributor registered successfully', distributor });
} catch (error) {
console.error('Error registering distributor:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get Distributor Details
const getDistributorDetails = async (req, res) => {
const userId = req.user.id;
try {
const distributor = await Distributor.findOne({ user: userId })
.populate('assignedProducts')
.populate('commissions');
if (!distributor) {
return res.status(404).json({ message: 'Distributor not found' });
}
res.status(200).json({ distributor });
} catch (error) {
console.error('Error fetching distributor details:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Assign Product to Distributor
const assignProductToDistributor = async (req, res) => {
const userId = req.user.id;
const { productId } = req.body;
try {
const distributor = await Distributor.findOne({ user: userId });
if (!distributor) {
return res.status(404).json({ message: 'Distributor not found' });
}
// Check if product is already assigned
if (distributor.assignedProducts.includes(productId)) {
return res.status(400).json({ message: 'Product already assigned to distributor' });
}
distributor.assignedProducts.push(productId);
await distributor.save();
res.status(200).json({ message: 'Product assigned to distributor successfully', distributor });
} catch (error) {
console.error('Error assigning product to distributor:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
registerDistributor,
getDistributorDetails,
assignProductToDistributor,
};
3.1.7. Define Routes
1. Affiliate Routes (routes/affiliateRoutes.js):
javascript
Copy code
// affiliate-distributor-service/routes/affiliateRoutes.js
const express = require('express');
const {
registerAffiliate,
getAffiliateDetails,
createReferral,
} = require('../controllers/affiliateController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Apply authentication middleware
router.use(authenticate);
// Route: POST /api/affiliates/register
router.post('/register', registerAffiliate);
// Route: GET /api/affiliates/me
router.get('/me', getAffiliateDetails);
// Route: POST /api/affiliates/referral
router.post('/referral', createReferral);
module.exports = router;
2. Distributor Routes (routes/distributorRoutes.js):
javascript
Copy code
// affiliate-distributor-service/routes/distributorRoutes.js
const express = require('express');
const {
registerDistributor,
getDistributorDetails,
assignProductToDistributor,
} = require('../controllers/distributorController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Apply authentication middleware
router.use(authenticate);
// Route: POST /api/distributors/register
router.post('/register', registerDistributor);
// Route: GET /api/distributors/me
router.get('/me', getDistributorDetails);
// Route: POST /api/distributors/assign-product
router.post('/assign-product', assignProductToDistributor);
module.exports = router;
3.1.8. Authentication Middleware (middleware/authenticate.js)
(Same as in previous stages)
javascript
Copy code
// affiliate-distributor-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
3.1.9. Testing Backend API Endpoints
• tests/affiliateController.test.js
javascript
Copy code
// affiliate-distributor-service/tests/affiliateController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Affiliate = require('../models/Affiliate');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test Affiliate',
email: 'testaffiliate@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Affiliate API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Affiliate.deleteMany();
});
describe('POST /api/affiliates/register', () => {
it('should register a new affiliate', async () => {
const res = await request(app)
.post('/api/affiliates/register')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Affiliate registered successfully');
expect(res.body).toHaveProperty('affiliate');
expect(res.body.affiliate).toHaveProperty('referralCode');
});
it('should not allow duplicate affiliate registration', async () => {
await Affiliate.create({
user: userPayload.id,
referralCode: 'abcd1234',
});
const res = await request(app)
.post('/api/affiliates/register')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User is already an affiliate');
});
});
describe('GET /api/affiliates/me', () => {
it('should retrieve affiliate details', async () => {
const affiliate = await Affiliate.create({
user: userPayload.id,
referralCode: 'abcd1234',
});
const res = await request(app)
.get('/api/affiliates/me')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('affiliate');
expect(res.body.affiliate).toHaveProperty('referralCode', 'abcd1234');
});
it('should return 404 if affiliate not found', async () => {
const res = await request(app)
.get('/api/affiliates/me')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Affiliate not found');
});
});
// Additional tests for referral creation can be implemented similarly
});
• tests/distributorController.test.js
javascript
Copy code
// affiliate-distributor-service/tests/distributorController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Distributor = require('../models/Distributor');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test Distributor',
email: 'testdistributor@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Distributor API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Distributor.deleteMany();
});
describe('POST /api/distributors/register', () => {
it('should register a new distributor', async () => {
const res = await request(app)
.post('/api/distributors/register')
.set('Authorization', `Bearer ${token}`)
.send({ region: 'North America' });
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Distributor registered successfully');
expect(res.body).toHaveProperty('distributor');
expect(res.body.distributor).toHaveProperty('region', 'North America');
});
it('should not allow duplicate distributor registration', async () => {
await Distributor.create({
user: userPayload.id,
region: 'Europe',
});
const res = await request(app)
.post('/api/distributors/register')
.set('Authorization', `Bearer ${token}`)
.send({ region: 'Asia' });
expect(res.statusCode).toEqual(400);
expect(res.body).toHaveProperty('message', 'User is already a distributor');
});
});
describe('GET /api/distributors/me', () => {
it('should retrieve distributor details', async () => {
const distributor = await Distributor.create({
user: userPayload.id,
region: 'Europe',
});
const res = await request(app)
.get('/api/distributors/me')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('distributor');
expect(res.body.distributor).toHaveProperty('region', 'Europe');
});
it('should return 404 if distributor not found', async () => {
const res = await request(app)
.get('/api/distributors/me')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Distributor not found');
});
});
// Additional tests for assigning products can be implemented similarly
});
• Run Backend Tests:
bash
Copy code
cd affiliate-distributor-service
npm test
3.1.10. Dockerize Affiliate & Distributor Management Service
1. Create Dockerfile:
dockerfile
Copy code
# affiliate-distributor-service/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install --production
# Bundle app source
COPY . .
# Create uploads directory (if needed)
RUN mkdir uploads
# Expose port
EXPOSE 5009
# Start the service
CMD [ "node", "index.js" ]
2. Update Root docker-compose.yml:
Add the Affiliate & Distributor Management Service to your Docker Compose configuration.
yaml
Copy code
# ibrandidit-microservices/docker-compose.yml
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://localhost:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://localhost:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
product-customization-service:
build: ./product-customization-service
restart: always
ports:
- "5008:5008"
environment:
- PORT=5008
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
affiliate-distributor-service:
build: ./affiliate-distributor-service
restart: always
ports:
- "5009:5009"
environment:
- PORT=5009
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/affiliatedb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
ports:
- "3000:3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
- REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
- REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
- affiliate-distributor-service
# ... other services
volumes:
mongo-data:
Notes:
o Service Names: Ensure that service names in Docker Compose are consistent and can communicate with each other via these names.
o Environment Variables: The frontend now includes REACT_APP_AFFILIATE_SERVICE_URL and REACT_APP_DISTRIBUTOR_SERVICE_URL pointing to the Affiliate & Distributor Management Service.
o Port Mapping: The Affiliate & Distributor Management Service runs on port 5009.
3. Start All Services:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend: Accessible at http://localhost:3000/
o Affiliate & Distributor Management Service: Accessible at http://localhost:5009/api/affiliates and http://localhost:5009/api/distributors
3.1.11. Backend Documentation (README.md)
markdown
Copy code
# Affiliate & Distributor Management Service
## Overview
The **Affiliate & Distributor Management Service** enables the management of affiliates and distributors who help promote and sell products on iBranDIDit.com. This service facilitates affiliate registration, tracking referrals and commissions, managing distributors, assigning products or regions, and monitoring performance metrics.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Service](#running-the-service)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- [MongoDB](https://www.mongodb.com/) instance running (local or cloud)
- Existing microservices setup (Auth, Order, Payment, Shipping, AI, Product Customization)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Service Directory:**
```bash
cd path/to/ibrandidit-microservices/affiliate-distributor-service
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the affiliate-distributor-service/ directory based on the provided .env.example.
env
Copy code
PORT=5009
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/affiliatedb?authSource=admin
JWT_SECRET=your_jwt_secret_key
RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
Descriptions:
• PORT: Port on which the service runs.
• NODE_ENV: Environment mode (development or production).
• MONGODB_URI: MongoDB connection string.
• JWT_SECRET: Secret key for JWT authentication.
• RABBITMQ_URL: RabbitMQ connection string for message queuing.
Note: Replace your_jwt_secret_key with your actual JWT secret key.
Running the Service
Option 1: Running Locally
1. Start the Service:
bash
Copy code
npm run dev
o The service will run on http://localhost:5009/.
Option 2: Using Docker
1. Build and Start the Service with Docker Compose:
Ensure that the service is included in your docker-compose.yml.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Affiliate & Distributor Management Service: Accessible at http://localhost:5009/api/affiliates and http://localhost:5009/api/distributors
API Documentation
Authentication
All API endpoints require authentication via JWT. Include the JWT token in the Authorization header as follows:
makefile
Copy code
Authorization: Bearer <token>
Affiliate Endpoints
1. Register as Affiliate
• Endpoint: POST /api/affiliates/register
• Description: Registers the authenticated user as an affiliate.
• Headers:
o Authorization: Bearer <token>
• Request Body: None
• Response:
o 201 Created
o json
Copy code
{
"message": "Affiliate registered successfully",
"affiliate": { ... }
}
2. Get Affiliate Details
• Endpoint: GET /api/affiliates/me
• Description: Retrieves details of the authenticated affiliate.
• Headers:
o Authorization: Bearer <token>
• Response:
o 200 OK
o json
Copy code
{
"affiliate": { ... }
}
o 404 Not Found if affiliate does not exist.
3. Create a Referral
• Endpoint: POST /api/affiliates/referral
• Description: Creates a referral using an affiliate's referral code.
• Headers:
o Authorization: Bearer <token>
• Request Body:
o referralCode (String, required): Affiliate's referral code.
o userId (String, required): ID of the referred user.
• Response:
o 201 Created
o json
Copy code
{
"message": "Referral created successfully",
"referral": { ... }
}
o 404 Not Found if referral code is invalid.
o 400 Bad Request if user already has a referral.
Distributor Endpoints
1. Register as Distributor
• Endpoint: POST /api/distributors/register
• Description: Registers the authenticated user as a distributor.
• Headers:
o Authorization: Bearer <token>
• Request Body:
o region (String, required): Region the distributor is responsible for.
• Response:
o 201 Created
o json
Copy code
{
"message": "Distributor registered successfully",
"distributor": { ... }
}
o 400 Bad Request if user is already a distributor.
2. Get Distributor Details
• Endpoint: GET /api/distributors/me
• Description: Retrieves details of the authenticated distributor.
• Headers:
o Authorization: Bearer <token>
• Response:
o 200 OK
o json
Copy code
{
"distributor": { ... }
}
o 404 Not Found if distributor does not exist.
3. Assign Product to Distributor
• Endpoint: POST /api/distributors/assign-product
• Description: Assigns a product to the authenticated distributor.
• Headers:
o Authorization: Bearer <token>
• Request Body:
o productId (String, required): ID of the product to assign.
• Response:
o 200 OK
o json
Copy code
{
"message": "Product assigned to distributor successfully",
"distributor": { ... }
}
o 404 Not Found if distributor is not found.
o 400 Bad Request if product is already assigned.
Error Responses
• 400 Bad Request: Missing or invalid data.
• 401 Unauthorized: Missing or invalid JWT token.
• 404 Not Found: Resource not found.
• 500 Internal Server Error: Server-side errors.
Testing
Backend Testing
1. Run Affiliate & Distributor Service Tests:
bash
Copy code
cd affiliate-distributor-service
npm test
o This command runs all tests using Jest and generates coverage reports.
2. Test Coverage:
o After running tests, a coverage/ directory is created containing detailed coverage reports.
Frontend Testing
(To be covered in the frontend section)
Linting and Formatting
Maintain code quality and consistency using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd affiliate-distributor-service
npm run lint
2. Run Formatting:
bash
Copy code
cd affiliate-distributor-service
npm run format
Deployment
Option 1: Docker Deployment
1. Build the Docker Image:
bash
Copy code
cd affiliate-distributor-service
docker build -t affiliate-distributor-service .
2. Run the Docker Container:
bash
Copy code
docker run -d -p 5009:5009 --env-file .env affiliate-distributor-service
o The service will be accessible at http://localhost:5009/api/affiliates and http://localhost:5009/api/distributors.
Option 2: Deploying to Cloud Providers
• Heroku, AWS, Google Cloud, etc.: Follow the respective provider's guidelines to deploy Node.js applications. Ensure environment variables are securely set.
Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/affiliate-feature
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add affiliate registration endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/affiliate-feature
o Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
bash
Copy code
#### **3.1.12. Finalizing Backend Code**
Ensure all necessary files are included and properly structured as per the project structure outlined above.
---
### **3.2. Frontend: Integrate Affiliate & Distributor Management Features**
Integrate the Affiliate & Distributor Management functionalities into the frontend React application.
#### **3.2.1. Update Environment Variables**
Ensure the frontend `.env` includes the Affiliate & Distributor Management Service URLs.
```env
REACT_APP_API_BASE_URL=http://order-service:5004
REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
Note: If running locally without Docker, replace service names with localhost and appropriate ports (e.g., http://localhost:5009/api/affiliates).
3.2.2. Install Additional Dependencies
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm install @mui/icons-material
3.2.3. Create Affiliate & Distributor Components
1. Affiliate Registration Component (src/components/Affiliate/AffiliateRegistration.js):
javascript
Copy code
// src/components/Affiliate/AffiliateRegistration.js
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Typography,
CircularProgress,
Paper,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const AffiliateRegistration = () => {
const token = useSelector((state) => state.auth.token);
const [loading, setLoading] = useState(false);
const [isAffiliate, setIsAffiliate] = useState(false);
const [affiliateData, setAffiliateData] = useState(null);
const AFFILIATE_API_URL = process.env.REACT_APP_AFFILIATE_SERVICE_URL;
useEffect(() => {
const checkAffiliateStatus = async () => {
try {
const res = await axios.get(`${AFFILIATE_API_URL}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
setIsAffiliate(true);
setAffiliateData(res.data.affiliate);
} catch (error) {
setIsAffiliate(false);
}
};
checkAffiliateStatus();
}, [AFFILIATE_API_URL, token]);
const handleRegister = async () => {
setLoading(true);
try {
const res = await axios.post(`${AFFILIATE_API_URL}/register`, {}, {
headers: { Authorization: `Bearer ${token}` },
});
setIsAffiliate(true);
setAffiliateData(res.data.affiliate);
toast.success('Registered as affiliate successfully');
} catch (error) {
toast.error(error.response?.data?.message || 'Registration failed');
} finally {
setLoading(false);
}
};
if (isAffiliate) {
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6">Affiliate Dashboard</Typography>
<Typography>Your Referral Code: {affiliateData.referralCode}</Typography>
{/* Additional affiliate details and statistics can be added here */}
</Paper>
);
}
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>
Become an Affiliate
</Typography>
<Typography gutterBottom>
Join our affiliate program and earn commissions by referring customers.
</Typography>
<Button
variant="contained"
color="primary"
onClick={handleRegister}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : 'Register as Affiliate'}
</Button>
</Paper>
);
};
export default AffiliateRegistration;
2. Affiliate Referral Tracking Component (src/components/Affiliate/AffiliateDashboard.js):
javascript
Copy code
// src/components/Affiliate/AffiliateDashboard.js
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
CircularProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Button,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const AffiliateDashboard = () => {
const token = useSelector((state) => state.auth.token);
const [affiliate, setAffiliate] = useState(null);
const [loading, setLoading] = useState(true);
const AFFILIATE_API_URL = process.env.REACT_APP_AFFILIATE_SERVICE_URL;
useEffect(() => {
const fetchAffiliateDetails = async () => {
try {
const res = await axios.get(`${AFFILIATE_API_URL}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
setAffiliate(res.data.affiliate);
} catch (error) {
toast.error('Failed to fetch affiliate details');
} finally {
setLoading(false);
}
};
fetchAffiliateDetails();
}, [AFFILIATE_API_URL, token]);
if (loading) {
return <CircularProgress />;
}
if (!affiliate) {
return null;
}
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>
Affiliate Dashboard
</Typography>
<Typography variant="subtitle1">
Referral Code: {affiliate.referralCode}
</Typography>
<Typography variant="subtitle1">
Status: {affiliate.status}
</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Referrals</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>User</TableCell>
<TableCell>Order</TableCell>
<TableCell>Status</TableCell>
<TableCell>Commission</TableCell>
</TableRow>
</TableHead>
<TableBody>
{affiliate.referrals.map((referral) => (
<TableRow key={referral._id}>
<TableCell>{referral.user.email}</TableCell>
<TableCell>{referral.order || 'N/A'}</TableCell>
<TableCell>{referral.status}</TableCell>
<TableCell>
{referral.commission
? `$${referral.commission.amount.toFixed(2)}`
: 'Pending'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</Paper>
);
};
export default AffiliateDashboard;
3. Distributor Registration Component (src/components/Distributor/DistributorRegistration.js):
javascript
Copy code
// src/components/Distributor/DistributorRegistration.js
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
Typography,
CircularProgress,
Paper,
TextField,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const DistributorRegistration = () => {
const token = useSelector((state) => state.auth.token);
const [loading, setLoading] = useState(false);
const [isDistributor, setIsDistributor] = useState(false);
const [distributorData, setDistributorData] = useState(null);
const [region, setRegion] = useState('');
const DISTRIBUTOR_API_URL = process.env.REACT_APP_DISTRIBUTOR_SERVICE_URL;
useEffect(() => {
const checkDistributorStatus = async () => {
try {
const res = await axios.get(`${DISTRIBUTOR_API_URL}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
setIsDistributor(true);
setDistributorData(res.data.distributor);
setRegion(res.data.distributor.region);
} catch (error) {
setIsDistributor(false);
}
};
checkDistributorStatus();
}, [DISTRIBUTOR_API_URL, token]);
const handleRegister = async () => {
if (!region) {
toast.error('Please enter a region');
return;
}
setLoading(true);
try {
const res = await axios.post(
`${DISTRIBUTOR_API_URL}/register`,
{ region },
{
headers: { Authorization: `Bearer ${token}` },
}
);
setIsDistributor(true);
setDistributorData(res.data.distributor);
setRegion(res.data.distributor.region);
toast.success('Registered as distributor successfully');
} catch (error) {
toast.error(error.response?.data?.message || 'Registration failed');
} finally {
setLoading(false);
}
};
if (isDistributor) {
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6">Distributor Dashboard</Typography>
<Typography>Your Region: {distributorData.region}</Typography>
{/* Additional distributor details and statistics can be added here */}
</Paper>
);
}
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>
Become a Distributor
</Typography>
<Typography gutterBottom>
Join our distribution network and expand our reach in your region.
</Typography>
<TextField
label="Region"
value={region}
onChange={(e) => setRegion(e.target.value)}
fullWidth
sx={{ mb: 2 }}
/>
<Button
variant="contained"
color="primary"
onClick={handleRegister}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : 'Register as Distributor'}
</Button>
</Paper>
);
};
export default DistributorRegistration;
4. Distributor Dashboard Component (src/components/Distributor/DistributorDashboard.js):
javascript
Copy code
// src/components/Distributor/DistributorDashboard.js
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
CircularProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Button,
TextField,
} from '@mui/material';
import axios from 'axios';
import { useSelector } from 'react-redux';
import { toast } from 'react-toastify';
const DistributorDashboard = () => {
const token = useSelector((state) => state.auth.token);
const [distributor, setDistributor] = useState(null);
const [loading, setLoading] = useState(true);
const [products, setProducts] = useState([]);
const [selectedProduct, setSelectedProduct] = useState('');
const DISTRIBUTOR_API_URL = process.env.REACT_APP_DISTRIBUTOR_SERVICE_URL;
const PRODUCT_API_URL = process.env.REACT_APP_API_BASE_URL; // Assuming Order Service handles products
useEffect(() => {
const fetchDistributorDetails = async () => {
try {
const res = await axios.get(`${DISTRIBUTOR_API_URL}/me`, {
headers: { Authorization: `Bearer ${token}` },
});
setDistributor(res.data.distributor);
} catch (error) {
toast.error('Failed to fetch distributor details');
} finally {
setLoading(false);
}
};
const fetchProducts = async () => {
try {
const res = await axios.get(`${PRODUCT_API_URL}/products`, {
headers: { Authorization: `Bearer ${token}` },
});
setProducts(res.data.products);
} catch (error) {
toast.error('Failed to fetch products');
}
};
fetchDistributorDetails();
fetchProducts();
}, [DISTRIBUTOR_API_URL, PRODUCT_API_URL, token]);
const handleAssignProduct = async () => {
if (!selectedProduct) {
toast.error('Please select a product to assign');
return;
}
try {
const res = await axios.post(
`${DISTRIBUTOR_API_URL}/assign-product`,
{ productId: selectedProduct },
{
headers: { Authorization: `Bearer ${token}` },
}
);
setDistributor(res.data.distributor);
setSelectedProduct('');
toast.success('Product assigned successfully');
} catch (error) {
toast.error(error.response?.data?.message || 'Failed to assign product');
}
};
if (loading) {
return <CircularProgress />;
}
if (!distributor) {
return null;
}
return (
<Paper sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom>
Distributor Dashboard
</Typography>
<Typography variant="subtitle1">Region: {distributor.region}</Typography>
<Typography variant="subtitle1">Sales: ${distributor.sales.toFixed(2)}</Typography>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Assigned Products</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Product Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Price</TableCell>
</TableRow>
</TableHead>
<TableBody>
{distributor.assignedProducts.map((product) => (
<TableRow key={product._id}>
<TableCell>{product.name}</TableCell>
<TableCell>{product.description}</TableCell>
<TableCell>${product.price.toFixed(2)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Assign New Product</Typography>
<TextField
select
label="Select Product"
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
SelectProps={{
native: true,
}}
fullWidth
sx={{ mb: 2 }}
>
<option value="">--Select--</option>
{products.map((product) => (
<option key={product._id} value={product._id}>
{product.name}
</option>
))}
</TextField>
<Button variant="contained" color="primary" onClick={handleAssignProduct}>
Assign Product
</Button>
</Box>
</Box>
<Box sx={{ mt: 4 }}>
<Typography variant="h6">Commissions</Typography>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{distributor.commissions.map((commission) => (
<TableRow key={commission._id}>
<TableCell>{new Date(commission.createdAt).toLocaleDateString()}</TableCell>
<TableCell>${commission.amount.toFixed(2)}</TableCell>
<TableCell>{commission.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
</Paper>
);
};
export default DistributorDashboard;
5. Affiliate Dashboard Component (src/components/Affiliate/AffiliateDashboard.js):
(Already provided above)
6. Integrate Affiliate & Distributor Components into Dashboard (src/pages/Dashboard/Dashboard.js):
javascript
Copy code
// src/pages/Dashboard/Dashboard.js
import React from 'react';
import { Typography, Box, Grid, Paper } from '@mui/material';
import { useSelector } from 'react-redux';
import Chatbot from '../../components/Chatbot/Chatbot';
import ImageGenerator from '../../components/ImageGenerator/ImageGenerator';
import CustomizationForm from '../../components/Customization/CustomizationForm';
import CustomizationList from '../../components/Customization/CustomizationList';
import TemplateList from '../../components/Customization/TemplateList';
import AffiliateRegistration from '../../components/Affiliate/AffiliateRegistration';
import AffiliateDashboard from '../../components/Affiliate/AffiliateDashboard';
import DistributorRegistration from '../../components/Distributor/DistributorRegistration';
import DistributorDashboard from '../../components/Distributor/DistributorDashboard';
const Dashboard = () => {
const user = useSelector((state) => state.auth.user);
return (
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
Welcome, {user.name}!
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Paper sx={{ padding: 2 }}>
<CustomizationForm />
</Paper>
</Grid>
<Grid item xs={12} md={8}>
<Paper sx={{ padding: 2 }}>
<CustomizationList />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<TemplateList />
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ padding: 2 }}>
<AffiliateRegistration />
<AffiliateDashboard />
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ padding: 2 }}>
<DistributorRegistration />
<DistributorDashboard />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<Chatbot />
</Paper>
</Grid>
<Grid item xs={12}>
<Paper sx={{ padding: 2 }}>
<ImageGenerator />
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default Dashboard;
3.2.4. Update Redux Store (if necessary)
Since the affiliate and distributor functionalities are handled independently, no specific Redux state management is required beyond existing configurations. However, ensure that Axios base URLs are correctly set up if needed.
3.2.5. Testing Frontend Components
1. Affiliate Registration Component Test (src/components/Affiliate/AffiliateRegistration.test.js):
javascript
Copy code
// src/components/Affiliate/AffiliateRegistration.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import AffiliateRegistration from './AffiliateRegistration';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('AffiliateRegistration Component', () => {
beforeEach(() => {
axios.get.mockResolvedValue({
data: {
affiliate: {
referralCode: 'abcd1234',
status: 'active',
referrals: [],
commissions: [],
},
},
});
});
test('renders affiliate registration form', async () => {
render(
<Provider store={store}>
<AffiliateRegistration />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Become an Affiliate/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Affiliate Dashboard/i)).toBeInTheDocument();
});
});
test('registers as affiliate successfully', async () => {
axios.post.mockResolvedValue({
data: {
affiliate: {
referralCode: 'abcd1234',
status: 'active',
referrals: [],
commissions: [],
},
},
});
render(
<Provider store={store}>
<AffiliateRegistration />
<ToastContainer />
</Provider>
);
// Click Register button
fireEvent.click(screen.getByRole('button', { name: /Register as Affiliate/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalled();
expect(screen.getByText(/Affiliate Dashboard/i)).toBeInTheDocument();
expect(screen.getByText(/Referral Code: abcd1234/i)).toBeInTheDocument();
expect(screen.getByText(/Registered as affiliate successfully/i)).toBeInTheDocument();
});
});
test('shows error when registration fails', async () => {
axios.post.mockRejectedValue({
response: { data: { message: 'Registration failed' } },
});
render(
<Provider store={store}>
<AffiliateRegistration />
<ToastContainer />
</Provider>
);
// Click Register button
fireEvent.click(screen.getByRole('button', { name: /Register as Affiliate/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalled();
expect(screen.getByText(/Registration failed/i)).toBeInTheDocument();
});
});
});
2. Distributor Registration Component Test (src/components/Distributor/DistributorRegistration.test.js):
javascript
Copy code
// src/components/Distributor/DistributorRegistration.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import DistributorRegistration from './DistributorRegistration';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('DistributorRegistration Component', () => {
beforeEach(() => {
axios.get.mockResolvedValue({
data: {
distributor: {
region: 'Europe',
status: 'active',
assignedProducts: [],
commissions: [],
sales: 0,
},
},
});
});
test('renders distributor registration form', async () => {
render(
<Provider store={store}>
<DistributorRegistration />
<ToastContainer />
</Provider>
);
expect(screen.getByText(/Become a Distributor/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Distributor Dashboard/i)).toBeInTheDocument();
});
});
test('registers as distributor successfully', async () => {
axios.post.mockResolvedValue({
data: {
distributor: {
region: 'Asia',
status: 'active',
assignedProducts: [],
commissions: [],
sales: 0,
},
},
});
render(
<Provider store={store}>
<DistributorRegistration />
<ToastContainer />
</Provider>
);
// Enter region
fireEvent.change(screen.getByLabelText(/Region/i), {
target: { value: 'Asia' },
});
// Click Register button
fireEvent.click(screen.getByRole('button', { name: /Register as Distributor/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalledWith(
'http://affiliate-distributor-service:5009/api/distributors/register',
{ region: 'Asia' },
{ headers: { Authorization: 'Bearer testtoken' } }
);
expect(screen.getByText(/Distributor Dashboard/i)).toBeInTheDocument();
expect(screen.getByText(/Region: Asia/i)).toBeInTheDocument();
expect(screen.getByText(/Registered as distributor successfully/i)).toBeInTheDocument();
});
});
test('shows error when registration fails', async () => {
axios.post.mockRejectedValue({
response: { data: { message: 'Registration failed' } },
});
render(
<Provider store={store}>
<DistributorRegistration />
<ToastContainer />
</Provider>
);
// Enter region
fireEvent.change(screen.getByLabelText(/Region/i), {
target: { value: 'Africa' },
});
// Click Register button
fireEvent.click(screen.getByRole('button', { name: /Register as Distributor/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalled();
expect(screen.getByText(/Registration failed/i)).toBeInTheDocument();
});
});
});
3. Affiliate Dashboard Component Test (src/components/Affiliate/AffiliateDashboard.test.js):
javascript
Copy code
// src/components/Affiliate/AffiliateDashboard.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import AffiliateDashboard from './AffiliateDashboard';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'John Doe' },
},
});
describe('AffiliateDashboard Component', () => {
beforeEach(() => {
axios.get.mockResolvedValue({
data: {
affiliate: {
referralCode: 'abcd1234',
status: 'active',
referrals: [
{
_id: 'ref1',
user: { email: 'referral1@example.com' },
order: 'order1',
status: 'completed',
commission: { amount: 10.5 },
},
{
_id: 'ref2',
user: { email: 'referral2@example.com' },
order: null,
status: 'pending',
commission: null,
},
],
},
},
});
});
test('renders affiliate dashboard with referrals', async () => {
render(
<Provider store={store}>
<AffiliateDashboard />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Affiliate Dashboard/i)).toBeInTheDocument();
expect(screen.getByText(/Referral Code: abcd1234/i)).toBeInTheDocument();
expect(screen.getByText(/referral1@example.com/i)).toBeInTheDocument();
expect(screen.getByText(/order1/i)).toBeInTheDocument();
expect(screen.getByText(/\$10.50/i)).toBeInTheDocument();
expect(screen.getByText(/referral2@example.com/i)).toBeInTheDocument();
expect(screen.getByText(/Pending/i)).toBeInTheDocument();
});
});
test('shows error when fetching affiliate details fails', async () => {
axios.get.mockRejectedValue(new Error('Failed to fetch affiliate'));
render(
<Provider store={store}>
<AffiliateDashboard />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Failed to fetch affiliate details/i)).toBeInTheDocument();
});
});
});
4. Distributor Dashboard Component Test (src/components/Distributor/DistributorDashboard.test.js):
javascript
Copy code
// src/components/Distributor/DistributorDashboard.test.js
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import DistributorDashboard from './DistributorDashboard';
import axios from 'axios';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { ToastContainer } from 'react-toastify';
jest.mock('axios');
const mockStore = configureStore([]);
const store = mockStore({
auth: {
token: 'testtoken',
user: { name: 'Jane Doe' },
},
});
describe('DistributorDashboard Component', () => {
beforeEach(() => {
axios.get.mockImplementation((url) => {
if (url.includes('/api/distributors/me')) {
return Promise.resolve({
data: {
distributor: {
region: 'Asia',
sales: 1500,
assignedProducts: [
{
_id: 'prod1',
name: 'Product 1',
description: 'Description 1',
price: 50,
},
],
commissions: [
{
_id: 'comm1',
amount: 100,
status: 'paid',
},
{
_id: 'comm2',
amount: 200,
status: 'pending',
},
],
},
},
});
}
if (url.includes('/api/products')) {
return Promise.resolve({
data: {
products: [
{ _id: 'prod1', name: 'Product 1' },
{ _id: 'prod2', name: 'Product 2' },
],
},
});
}
return Promise.reject(new Error('Not Found'));
});
});
test('renders distributor dashboard with assigned products and commissions', async () => {
render(
<Provider store={store}>
<DistributorDashboard />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Distributor Dashboard/i)).toBeInTheDocument();
expect(screen.getByText(/Region: Asia/i)).toBeInTheDocument();
expect(screen.getByText(/Sales: \$1500.00/i)).toBeInTheDocument();
// Check assigned products
expect(screen.getByText(/Product 1/i)).toBeInTheDocument();
expect(screen.getByText(/Description 1/i)).toBeInTheDocument();
expect(screen.getByText(/\$50.00/i)).toBeInTheDocument();
// Check commissions
expect(screen.getByText(/\$100.00/i)).toBeInTheDocument();
expect(screen.getByText(/paid/i)).toBeInTheDocument();
expect(screen.getByText(/\$200.00/i)).toBeInTheDocument();
expect(screen.getByText(/pending/i)).toBeInTheDocument();
});
});
test('assigns a product to distributor successfully', async () => {
axios.post.mockResolvedValue({
data: {
distributor: {
region: 'Asia',
sales: 1500,
assignedProducts: [
{
_id: 'prod1',
name: 'Product 1',
description: 'Description 1',
price: 50,
},
{
_id: 'prod2',
name: 'Product 2',
description: 'Description 2',
price: 75,
},
],
commissions: [
{
_id: 'comm1',
amount: 100,
status: 'paid',
},
{
_id: 'comm2',
amount: 200,
status: 'pending',
},
],
},
},
});
render(
<Provider store={store}>
<DistributorDashboard />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Distributor Dashboard/i)).toBeInTheDocument();
});
// Assign Product 2
fireEvent.change(screen.getByLabelText(/Select Product/i), {
target: { value: 'prod2' },
});
fireEvent.click(screen.getByRole('button', { name: /Assign Product/i }));
await waitFor(() => {
expect(axios.post).toHaveBeenCalledWith(
'http://affiliate-distributor-service:5009/api/distributors/assign-product',
{ productId: 'prod2' },
{ headers: { Authorization: 'Bearer testtoken' } }
);
expect(screen.getByText(/Product assigned successfully/i)).toBeInTheDocument();
expect(screen.getByText(/Product 2/i)).toBeInTheDocument();
expect(screen.getByText(/Description 2/i)).toBeInTheDocument();
expect(screen.getByText(/\$75.00/i)).toBeInTheDocument();
});
});
test('shows error when assigning product fails', async () => {
axios.post.mockRejectedValue({
response: { data: { message: 'Failed to assign product' } },
});
render(
<Provider store={store}>
<DistributorDashboard />
<ToastContainer />
</Provider>
);
await waitFor(() => {
expect(screen.getByText(/Distributor Dashboard/i)).toBeInTheDocument();
});
// Assign Product without selecting
fireEvent.click(screen.getByRole('button', { name: /Assign Product/i }));
await waitFor(() => {
expect(screen.getByText(/Please select a product to assign/i)).toBeInTheDocument();
});
});
});
5. Run Frontend Tests:
bash
Copy code
cd path/to/ibrandidit-microservices/frontend/user-dashboard
npm test
o Ensure all tests pass, especially those related to affiliate and distributor functionalities.
3.2.6. Frontend Documentation (README.md)
Update the README.md in the frontend/user-dashboard directory to include information about the Affiliate & Distributor Management features.
markdown
Copy code
# User Dashboard
## Overview
The **User Dashboard** is a React.js frontend application that provides users with a centralized platform to manage their profiles, view order history, track shipments, manage payments, interact with AI-powered features like chatbots and image generation, customize products and templates, manage affiliates and distributors, subscribe to newsletters, and engage with social media. This dashboard integrates seamlessly with backend microservices, ensuring a smooth and secure user experience.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Environment Variables](#environment-variables)
- [Running the Application](#running-the-application)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Linting and Formatting](#linting-and-formatting)
- [Deployment](#deployment)
- [Contribution](#contribution)
- [License](#license)
## Prerequisites
- [Node.js](https://nodejs.org/) v14 or higher
- [NPM](https://www.npmjs.com/) (comes with Node.js)
- Backend Microservices running (Auth, Order, Payment, Shipping, AI, Product Customization, Affiliate & Distributor)
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) (optional, for containerization)
## Installation
1. **Navigate to the Frontend Directory:**
```bash
cd path/to/ibrandidit-microservices/frontend/user-dashboard
2. Install Dependencies:
bash
Copy code
npm install
Environment Variables
Create a .env file in the user-dashboard/ directory based on the provided .env.example.
env
Copy code
REACT_APP_API_BASE_URL=http://order-service:5004
REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
Descriptions:
• REACT_APP_API_BASE_URL: Base URL for the Order Service.
• REACT_APP_SHIPPING_SERVICE_URL: Base URL for the Shipping Service.
• REACT_APP_PAYMENT_SERVICE_URL: Base URL for the Payment Service.
• REACT_APP_AUTH_SERVICE_URL: Base URL for the Auth Service.
• REACT_APP_GOOGLE_ANALYTICS_ID: Your Google Analytics Tracking ID.
• REACT_APP_AI_SERVICE_URL: Base URL for the AI Service.
• REACT_APP_CUSTOMIZATION_SERVICE_URL: Base URL for the Product Customization Service.
• REACT_APP_AFFILIATE_SERVICE_URL: Base URL for the Affiliate Service.
• REACT_APP_DISTRIBUTOR_SERVICE_URL: Base URL for the Distributor Service.
Note: Replace the URLs with actual service endpoints if using Docker Compose with service names (e.g., http://localhost:5009/api/affiliates).
Running the Application
Option 1: Running Locally
1. Start Backend Microservices:
Ensure that all backend services (Auth, Order, Payment, Shipping, AI, Product Customization, Affiliate & Distributor) are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up -d
2. Start the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm start
o The application will run on http://localhost:3000/.
o Login/Register to access the dashboard.
Option 2: Using Docker
1. Ensure Docker Compose is Updated:
Verify that docker-compose.yml includes the Affiliate & Distributor Management Service and that all environment variables are correctly set.
2. Start All Services with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend: Accessible at http://localhost:3000/.
o Affiliate & Distributor Management Service: Accessible at http://localhost:5009/api/affiliates and http://localhost:5009/api/distributors
API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Auth Service: http://localhost:5003/api-docs
• Order Service: http://localhost:5004/api-docs
• Payment Service: http://localhost:5005/api-docs
• Shipping Service: http://localhost:5006/api-docs
• AI Service: http://localhost:5007/api-docs
• Product Customization Service: http://localhost:5008/api-docs
• Affiliate & Distributor Management Service: http://localhost:5009/api-docs
Testing
Testing ensures that the Affiliate & Distributor Management integrations work as expected.
Backend Testing
1. Run Affiliate & Distributor Service Tests:
bash
Copy code
cd affiliate-distributor-service
npm test
o Ensure all tests pass, verifying the functionality of affiliate and distributor endpoints.
Frontend Testing
1. Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
o Ensure that all tests pass, especially those related to affiliate and distributor functionalities.
Linting and Formatting
Maintain code quality using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd affiliate-distributor-service
npm run lint
2. Run Formatting:
bash
Copy code
cd affiliate-distributor-service
npm run format
Deployment
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
o This command creates a production-ready build in the build/ directory.
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend will be accessible at http://localhost:3000/.
o Ensure that all environment variables are correctly set in the .env files.
o Replace placeholder values with actual credentials before deploying to production.
Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/affiliate-feature
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add affiliate registration endpoint"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/affiliate-feature
o Submit a pull request from your fork to the main repository for review.
License
This project is licensed under the MIT License.
________________________________________
arduino
Copy code
#### **3.2.7. Frontend: Customization Service Integration**
1. **Fetch Affiliates and Distributors:**
Ensure that the Affiliate Registration and Distributor Registration components interact correctly with their respective backend services.
- **Modify `CustomizationForm.js` (if needed):**
Not directly related to affiliates/distributors but ensure that forms do not conflict.
2. **Handle JWT Authentication:**
All API calls must include the JWT token in the `Authorization` header.
- **Ensure Axios is configured with interceptors to include the token:**
*(Already handled in `api.js` and other service integrations)*
3. **Error Handling and Notifications:**
Use `React Toastify` to display success and error messages based on API responses.
- **Example:**
```javascript
toast.success('Registered as affiliate successfully');
toast.error('Registration failed');
```
4. **Responsive Design:**
Ensure that affiliate and distributor registration forms and dashboards are responsive and accessible across various devices and screen sizes.
- Utilize Material-UI's Grid system and responsive components.
#### **3.2.8. Update Frontend API Service (`src/services/api.js`)**
If you have a centralized API service, ensure it includes endpoints for Affiliates and Distributors.
```javascript
// src/services/api.js
import axios from 'axios';
import store from '../redux/store';
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL;
const CUSTOMIZATION_SERVICE_URL = process.env.REACT_APP_CUSTOMIZATION_SERVICE_URL;
const AFFILIATE_SERVICE_URL = process.env.REACT_APP_AFFILIATE_SERVICE_URL;
const DISTRIBUTOR_SERVICE_URL = process.env.REACT_APP_DISTRIBUTOR_SERVICE_URL;
const api = axios.create({
baseURL: API_BASE_URL,
});
// Request interceptor to add JWT token
api.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Export APIs for different services
export const customizationApi = axios.create({
baseURL: CUSTOMIZATION_SERVICE_URL,
headers: {
'Content-Type': 'application/json',
},
});
customizationApi.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export const affiliateApi = axios.create({
baseURL: AFFILIATE_SERVICE_URL,
headers: {
'Content-Type': 'application/json',
},
});
affiliateApi.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export const distributorApi = axios.create({
baseURL: DISTRIBUTOR_SERVICE_URL,
headers: {
'Content-Type': 'application/json',
},
});
distributorApi.interceptors.request.use(
(config) => {
const state = store.getState();
const token = state.auth.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default api;
3.2.9. Finalizing Frontend Code
Ensure that all components are correctly imported and integrated into the dashboard. Also, make sure that navigation routes (if any) are updated accordingly.
________________________________________
4. Documentation and Usage
4.1. Running the Affiliate & Distributor Management Service Locally
1. Start the Affiliate & Distributor Service:
bash
Copy code
cd path/to/ibrandidit-microservices/affiliate-distributor-service
npm run dev
o The service will run on http://localhost:5009/.
2. Ensure Backend Services Are Running:
o Auth, Order, Payment, Shipping, AI, Product Customization services should be running and accessible.
4.2. Interacting with Affiliate & Distributor Features
1. Affiliate Management
• Register as Affiliate:
o Accessing the Registration:
Navigate to http://localhost:3000/dashboard after logging in.
Locate the Become an Affiliate section.
o Registering:
Click the Register as Affiliate button.
Upon successful registration, you'll receive a unique referral code.
• Affiliate Dashboard:
o Referral Code: Your unique code to share with potential customers.
o Referrals: View users you've referred and their order statuses.
o Commissions: Track earnings from completed referrals.
2. Distributor Management
• Register as Distributor:
o Accessing the Registration:
Navigate to http://localhost:3000/dashboard after logging in.
Locate the Become a Distributor section.
o Registering:
Enter the region you're responsible for.
Click the Register as Distributor button.
• Distributor Dashboard:
o Region: The area you're managing.
o Assigned Products: Products assigned to you for distribution.
o Sales: Total sales generated.
o Commissions: Track earnings from sales.
• Assign Products:
o Select products from the dropdown and assign them to your distributor account.
o View assigned products in the dashboard.
4.3. API Documentation
The Affiliate & Distributor Management Service exposes the following endpoints:
1. Affiliate Endpoints
o Register as Affiliate
Endpoint: POST /api/affiliates/register
Description: Registers the authenticated user as an affiliate.
Headers:
Authorization: Bearer <token>
Request Body: None
Response:
201 Created
json
Copy code
{
"message": "Affiliate registered successfully",
"affiliate": { ... }
}
o Get Affiliate Details
Endpoint: GET /api/affiliates/me
Description: Retrieves details of the authenticated affiliate.
Headers:
Authorization: Bearer <token>
Response:
200 OK
json
Copy code
{
"affiliate": { ... }
}
404 Not Found if affiliate does not exist.
o Create a Referral
Endpoint: POST /api/affiliates/referral
Description: Creates a referral using an affiliate's referral code.
Headers:
Authorization: Bearer <token>
Request Body:
referralCode (String, required): Affiliate's referral code.
userId (String, required): ID of the referred user.
Response:
201 Created
json
Copy code
{
"message": "Referral created successfully",
"referral": { ... }
}
404 Not Found if referral code is invalid.
400 Bad Request if user already has a referral.
2. Distributor Endpoints
o Register as Distributor
Endpoint: POST /api/distributors/register
Description: Registers the authenticated user as a distributor.
Headers:
Authorization: Bearer <token>
Request Body:
region (String, required): Region the distributor is responsible for.
Response:
201 Created
json
Copy code
{
"message": "Distributor registered successfully",
"distributor": { ... }
}
400 Bad Request if user is already a distributor.
o Get Distributor Details
Endpoint: GET /api/distributors/me
Description: Retrieves details of the authenticated distributor.
Headers:
Authorization: Bearer <token>
Response:
200 OK
json
Copy code
{
"distributor": { ... }
}
404 Not Found if distributor does not exist.
o Assign Product to Distributor
Endpoint: POST /api/distributors/assign-product
Description: Assigns a product to the authenticated distributor.
Headers:
Authorization: Bearer <token>
Request Body:
productId (String, required): ID of the product to assign.
Response:
200 OK
json
Copy code
{
"message": "Product assigned to distributor successfully",
"distributor": { ... }
}
404 Not Found if distributor is not found.
400 Bad Request if product is already assigned.
4.4. Testing
1. Backend Testing:
o Run Affiliate & Distributor Service Tests:
bash
Copy code
cd affiliate-distributor-service
npm test
Ensure all tests pass, verifying the functionality of affiliate and distributor endpoints.
2. Frontend Testing:
o Run Frontend Tests:
bash
Copy code
cd frontend/user-dashboard
npm test
Ensure that all tests pass, especially those related to affiliate and distributor functionalities.
4.5. Linting and Formatting
Maintain code quality using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd affiliate-distributor-service
npm run lint
2. Run Formatting:
bash
Copy code
cd affiliate-distributor-service
npm run format
4.6. Deployment
1. Build the Frontend Application:
bash
Copy code
cd frontend/user-dashboard
npm run build
o This command creates a production-ready build in the build/ directory.
2. Serve the Build Locally (for Testing):
bash
Copy code
npm install -g serve
serve -s build
o The application will run on http://localhost:5000/ by default.
3. Deploying with Docker:
If you've set up Docker, the Dockerfile will build the production-ready application, which can be deployed to any Docker-compatible hosting service.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build
o Frontend will be accessible at http://localhost:3000/.
o Ensure that all environment variables are correctly set in the .env files.
o Replace placeholder values with actual credentials before deploying to production.
________________________________________
5. Integration with Previous Services
Ensure that the Affiliate & Distributor Management Service seamlessly interacts with existing microservices and frontend components.
5.1. Auth Service Integration
• Secure Affiliate & Distributor Endpoints:
o All endpoints are secured using JWT authentication.
o The frontend includes the JWT token in the Authorization header for all API requests.
5.2. Order Service Integration
• Referral Tracking:
o When an affiliate refers a user, and that user places an order, the Order Service should notify the Affiliate Service to update the referral status and calculate commissions.
o Implement Event-Based Communication:
Utilize RabbitMQ for communication between services.
When an order is completed, publish an event to RabbitMQ.
The Affiliate Service listens to order completion events to update referrals and commissions.
o Example:
Order Service: Publishes an event order.completed with order details.
Affiliate Service: Subscribes to order.completed events, finds the corresponding referral, updates its status to completed, and creates a commission record.
5.3. Payment Service Integration
• Commission Payouts:
o The Payment Service handles payouts to affiliates and distributors.
o Upon commission approval, the Affiliate Service requests the Payment Service to process the payout.
5.4. Frontend Dashboard Integration
• Affiliate and Distributor Components:
o The dashboard includes sections for affiliates and distributors, allowing users to register, view their referrals, commissions, assigned products, and performance metrics.
• Navigation and Access Control:
o Ensure that only authorized users can access affiliate and distributor features.
o Modify the PrivateRoute component if necessary to handle different user roles.
________________________________________
6. Additional Tools and Best Practices
1. Rate Limiting:
o Prevent abuse by limiting the number of affiliate and distributor registrations or updates per user/IP.
o Already implemented using express-rate-limit in the backend.
2. Caching:
o Implement caching for frequently accessed data, such as affiliate and distributor details, to reduce database load and improve response times.
o Use node-cache or similar libraries in the backend.
3. Logging:
o Implement comprehensive logging to monitor service performance and debug issues.
o Use winston for structured logging in the backend.
4. Security:
o Input Validation: Use libraries like express-validator to validate and sanitize user inputs.
o Helmet: Install and use helmet to set secure HTTP headers.
o HTTPS: Ensure all communications are over HTTPS in production environments.
5. Error Handling:
o Implement centralized error handling to provide meaningful feedback to users and maintain application stability.
o Use error-handling middleware in the backend.
6. Responsive Design:
o Ensure that affiliate and distributor registration forms and dashboards are responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
7. Version Control:
o Commit changes regularly with clear messages.
o Use feature branches for new functionalities.
o Merge changes via pull requests with code reviews.
________________________________________
7. Troubleshooting Tips
1. Backend Service Not Running:
o Problem: Affiliate & Distributor Management Service endpoints are unresponsive.
o Solution:
Ensure that the service is running by checking Docker containers.
Review service logs for any errors.
Verify that MongoDB is connected successfully.
2. CORS Errors:
o Problem: Frontend cannot communicate with the Affiliate & Distributor Service due to CORS policy.
o Solution:
Ensure that the Affiliate & Distributor Service has CORS enabled (app.use(cors())).
Verify that the frontend origin is allowed in the CORS configuration.
3. Commission Calculation Issues:
o Problem: Commissions are not being calculated correctly after referrals.
o Solution:
Verify the event-based communication between the Order Service and Affiliate Service.
Ensure that the correct referral is being updated upon order completion.
Check commission calculation logic for accuracy.
4. JWT Authentication Errors:
o Problem: Frontend receives 401 Unauthorized errors when accessing affiliate or distributor endpoints.
o Solution:
Ensure that JWT tokens are correctly stored and included in API requests.
Verify that the JWT_SECRET is consistent across services.
Check that the authentication middleware is correctly implemented in the backend.
5. Database Connection Issues:
o Problem: Services cannot connect to MongoDB.
o Solution:
Ensure that MongoDB is running and accessible.
Verify the MONGODB_URI in the .env files.
Check network configurations and firewall settings.
6. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
7. Form Validation Errors:
o Problem: Users cannot submit affiliate or distributor registration forms due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
8. Deployment Challenges:
o Problem: Affiliate & Distributor features work locally but fail after deployment.
o Solution:
Ensure that environment variables are correctly set in the production environment.
Verify that backend services are accessible from the frontend in the production setup.
Check for any build or runtime errors during deployment.
________________________________________
8. Additional Resources
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Redux Documentation: https://redux.js.org/introduction/getting-started
• Material-UI Documentation: https://mui.com/getting-started/usage/
• Formik Documentation: https://formik.org/docs/overview
• Yup Documentation: https://github.com/jquense/yup
• Axios Documentation: https://axios-http.com/docs/intro
• React Toastify Documentation: https://fkhadra.github.io/react-toastify/introduction
• Jest Documentation: https://jestjs.io/docs/getting-started
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Passport.js Documentation: http://www.passportjs.org/docs/
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• ESLint Documentation: https://eslint.org/docs/user-guide/getting-started
• Prettier Documentation: https://prettier.io/docs/en/index.html
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 17: Affiliate & Distributor Management within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced commission structures, enhancing security measures, or integrating with third-party services for payouts.
________________________________________
By following this comprehensive guide for Stage 17: Affiliate & Distributor Management, you've successfully expanded your eCommerce platform's capabilities to include robust affiliate and distributor management features. These functionalities not only enhance your marketing strategies but also foster collaborative partnerships that drive sustained growth and success.
Next Steps:
Proceed to Stage 18: Inventory Management Integration to further refine your backend services, ensuring efficient stock tracking and synchronization across your platform.
Stage 18: Domain & Subsite Integration is a crucial phase that ensures your eCommerce platform can effectively manage multiple domains and subsites. This integration allows for better organization, enhanced SEO capabilities, and tailored user experiences across different segments of your platform. Below is a comprehensive guide to implementing this stage, complete with code, dependencies, configuration files, and documentation suitable for beginners.
________________________________________
Stage 18: Domain & Subsite Integration
________________________________________
1. Overview
Objective:
• Multi-Domain Support:
o Host multiple domains (e.g., shop.ibrandidit.com, blog.ibrandidit.com) pointing to different subsites.
• Subsite Management:
o Create and manage subsites under different subdomains or domains.
o Ensure each subsite can operate independently while sharing common resources.
• Routing and Reverse Proxy Setup:
o Use a reverse proxy (Nginx) to route incoming requests to the appropriate backend services based on the domain or subdomain.
• SSL/TLS Configuration:
o Secure all domains and subsites with SSL certificates for HTTPS support.
Benefits:
• Enhanced SEO: Different subsites can target specific keywords and audiences.
• Organized Structure: Separate concerns by dividing the platform into distinct domains or subsites.
• Scalability: Easily add new domains or subsites as the business grows.
• Tailored User Experience: Customize each subsite to cater to different user needs or regional preferences.
________________________________________
2. Technology Stack
• Reverse Proxy: Nginx – For routing requests based on domain or subdomain.
• Containerization: Docker – Continuation from previous stages.
• Orchestration: Docker Compose – For managing multi-container Docker applications.
• SSL/TLS Certificates: Let's Encrypt via Certbot – For securing domains.
• DNS Management: Your domain registrar's DNS settings – For pointing domains to your server.
• Frontend Framework: React.js – Continuation from previous stages.
• Backend Framework: Node.js with Express.js – Continuation from previous stages.
• Load Balancing: Nginx will also handle load balancing if necessary.
________________________________________
3. Implementation Steps
3.1. Setting Up DNS Records
Before configuring your server, ensure that your domains and subdomains point to your server's IP address.
1. Access Your Domain Registrar:
o Log in to your domain registrar's dashboard (e.g., GoDaddy, Namecheap).
2. Create DNS A Records:
o Primary Domain:
Host: @
Points to: Your server's IP address (e.g., 123.456.789.101)
o Subdomains:
Host: shop
Points to: Your server's IP address
Host: blog
Points to: Your server's IP address
o Repeat for any additional subdomains.
3. Save DNS Settings:
o DNS propagation may take up to 24-48 hours, but typically completes within a few hours.
3.2. Configuring Nginx as a Reverse Proxy
Nginx will act as a reverse proxy, directing traffic to the appropriate services based on the requested domain or subdomain.
3.2.1. Install Nginx
If Nginx isn't already installed on your server, install it using the following commands:
bash
Copy code
# For Ubuntu/Debian-based systems
sudo apt update
sudo apt install nginx
# For CentOS/RHEL-based systems
sudo yum install epel-release
sudo yum install nginx
# Start and enable Nginx
sudo systemctl start nginx
sudo systemctl enable nginx
3.2.2. Configure Nginx Server Blocks
Create separate server blocks for each domain and subdomain.
1. Navigate to Nginx Configuration Directory:
bash
Copy code
cd /etc/nginx/sites-available/
2. Create Server Block for Primary Domain (ibrandidit.com):
bash
Copy code
sudo nano ibrandidit.com.conf
Content:
nginx
Copy code
server {
listen 80;
server_name ibrandidit.com www.ibrandidit.com;
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name ibrandidit.com www.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
3. Create Server Block for Subdomain shop.ibrandidit.com:
bash
Copy code
sudo nano shop.ibrandidit.com.conf
Content:
nginx
Copy code
server {
listen 80;
server_name shop.ibrandidit.com;
location / {
proxy_pass http://shop-frontend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name shop.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/shop.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://shop-frontend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
4. Create Server Block for Subdomain blog.ibrandidit.com:
bash
Copy code
sudo nano blog.ibrandidit.com.conf
Content:
nginx
Copy code
server {
listen 80;
server_name blog.ibrandidit.com;
location / {
proxy_pass http://blog-frontend:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name blog.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/blog.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://blog-frontend:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
5. Enable Server Blocks:
Create symbolic links in the sites-enabled directory.
bash
Copy code
sudo ln -s /etc/nginx/sites-available/ibrandidit.com.conf /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/shop.ibrandidit.com.conf /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/blog.ibrandidit.com.conf /etc/nginx/sites-enabled/
6. Test Nginx Configuration:
bash
Copy code
sudo nginx -t
o If the output is successful:
vbnet
Copy code
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
o If there are errors, review the configuration files for mistakes.
7. Reload Nginx:
bash
Copy code
sudo systemctl reload nginx
3.3. Obtaining SSL Certificates with Let's Encrypt
Use Certbot to obtain SSL certificates for your domains and subdomains.
1. Install Certbot and Nginx Plugin:
bash
Copy code
# For Ubuntu/Debian-based systems
sudo apt install certbot python3-certbot-nginx
# For CentOS/RHEL-based systems
sudo yum install certbot python3-certbot-nginx
2. Obtain Certificates:
o Primary Domain:
bash
Copy code
sudo certbot --nginx -d ibrandidit.com -d www.ibrandidit.com
o Subdomain shop.ibrandidit.com:
bash
Copy code
sudo certbot --nginx -d shop.ibrandidit.com
o Subdomain blog.ibrandidit.com:
bash
Copy code
sudo certbot --nginx -d blog.ibrandidit.com
o Follow the interactive prompts to complete the installation.
3. Automate Certificate Renewal:
Certbot sets up automatic renewal by default. To test the renewal process, run:
bash
Copy code
sudo certbot renew --dry-run
3.4. Updating Docker Compose Configuration
Ensure that your Docker Compose setup includes frontend services for each domain or subdomain.
1. Navigate to Your Docker Compose File:
bash
Copy code
cd path/to/ibrandidit-microservices/
2. Update docker-compose.yml:
yaml
Copy code
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
# ... Other backend services
frontend:
build: ./frontend/user-dashboard
restart: always
expose:
- "3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
- REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
- REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
- affiliate-distributor-service
shop-frontend:
build: ./frontend/shop-dashboard
restart: always
expose:
- "3001"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
# ... other environment variables specific to shop
depends_on:
- auth-service
- order-service
# ... other dependencies
blog-frontend:
build: ./frontend/blog-dashboard
restart: always
expose:
- "3002"
environment:
- REACT_APP_API_BASE_URL=http://blog-service:5010
# ... other environment variables specific to blog
depends_on:
- auth-service
- blog-service
# ... other dependencies
# Nginx reverse proxy
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- shop-frontend
- blog-frontend
volumes:
mongo-data:
Notes:
o Expose vs. Ports:
Use expose instead of ports for internal communication between containers.
o Nginx Service:
Mount the Nginx configuration directory and SSL certificates.
Ensure that the Nginx service depends on the frontend services.
3. Create Frontend Services for Subdomains:
o Shop Frontend (./frontend/shop-dashboard):
Set up a separate React application or a specialized frontend build for the shop subsite.
o Blog Frontend (./frontend/blog-dashboard):
Set up a separate React application or a specialized frontend build for the blog subsite.
Example:
bash
Copy code
# Create directories for shop and blog frontends
mkdir -p frontend/shop-dashboard
mkdir -p frontend/blog-dashboard
# Initialize React apps (if not already done)
npx create-react-app frontend/shop-dashboard
npx create-react-app frontend/blog-dashboard
o Customize each frontend application as needed for their respective subsites.
4. Rebuild and Restart Docker Containers:
bash
Copy code
docker-compose up --build -d
o Ensure all services are up and running:
bash
Copy code
docker-compose ps
3.5. Frontend Configuration for Subsites
Ensure that each frontend application is correctly configured to communicate with its respective backend services.
1. Shop Frontend (frontend/shop-dashboard/.env):
env
Copy code
REACT_APP_API_BASE_URL=http://order-service:5004
# Add other environment variables specific to the shop
2. Blog Frontend (frontend/blog-dashboard/.env):
env
Copy code
REACT_APP_API_BASE_URL=http://blog-service:5010
# Add other environment variables specific to the blog
3. Update API Service (frontend/user-dashboard/src/services/api.js):
o Add separate API instances if necessary for shop and blog.
o Ensure that Axios is correctly configured with interceptors to include JWT tokens.
3.6. Backend Services for Subsites
Create or configure backend services that correspond to each subsite.
1. Shop Service:
o Create a new backend service (e.g., shop-service) to handle shop-specific APIs.
o Implement necessary routes, controllers, and models.
2. Blog Service:
o Create a new backend service (e.g., blog-service) to handle blog-specific APIs.
o Implement necessary routes, controllers, and models.
Example Directory Structure:
bash
Copy code
blog-service/
├── controllers/
├── models/
├── routes/
├── tests/
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
3. Integrate with Nginx:
o Ensure that the Nginx configuration routes traffic for shop.ibrandidit.com to shop-frontend:3001 and blog.ibrandidit.com to blog-frontend:3002.
3.7. Testing the Integration
1. Accessing the Primary Domain:
o URL: http://ibrandidit.com or https://ibrandidit.com
o Expected Behavior: Loads the main frontend application.
2. Accessing the Shop Subdomain:
o URL: http://shop.ibrandidit.com or https://shop.ibrandidit.com
o Expected Behavior: Loads the shop frontend application.
3. Accessing the Blog Subdomain:
o URL: http://blog.ibrandidit.com or https://blog.ibrandidit.com
o Expected Behavior: Loads the blog frontend application.
4. Verify SSL Certificates:
o Ensure that all domains and subdomains are accessible over HTTPS without certificate warnings.
5. API Communication:
o Test API calls from each frontend application to their respective backend services.
o Ensure that authentication tokens are correctly handled.
6. Error Handling:
o Attempt to access a non-configured domain to ensure Nginx responds appropriately (e.g., 404 Not Found).
3.8. Documentation (README.md)
Update the README.md in the frontend/user-dashboard, frontend/shop-dashboard, frontend/blog-dashboard, and respective backend services to include information about Domain & Subsite Integration.
markdown
Copy code
# Domain & Subsite Integration
## Overview
**Domain & Subsite Integration** enables iBranDIDit.com to manage multiple domains and subsites efficiently. This setup allows the platform to host distinct sections such as the main store, shop, and blog under different subdomains, each with its own frontend and backend services.
## Table of Contents
- [Prerequisites](#prerequisites)
- [DNS Configuration](#dns-configuration)
- [Reverse Proxy with Nginx](#reverse-proxy-with-nginx)
- [SSL/TLS Setup](#ssltls-setup)
- [Docker Compose Configuration](#docker-compose-configuration)
- [Frontend Setup](#frontend-setup)
- [Primary Frontend](#primary-frontend)
- [Shop Frontend](#shop-frontend)
- [Blog Frontend](#blog-frontend)
- [Backend Services Setup](#backend-services-setup)
- [Primary Backend](#primary-backend)
- [Shop Backend](#shop-backend)
- [Blog Backend](#blog-backend)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)
- [Contribution Guidelines](#contribution-guidelines)
- [License](#license)
## Prerequisites
- Existing microservices setup (Auth, Order, Payment, Shipping, AI, Product Customization, Affiliate & Distributor, Inventory Management)
- Docker and Docker Compose installed
- Nginx installed on the server
- Domains and subdomains registered and pointing to your server's IP
## DNS Configuration
1. **Access Your Domain Registrar:**
- Log in to your domain registrar's dashboard (e.g., GoDaddy, Namecheap).
2. **Create DNS A Records:**
- **Primary Domain:**
- **Host:** `@`
- **Points to:** Your server's IP address (e.g., `123.456.789.101`)
- **Subdomains:**
- **Host:** `shop`
- **Points to:** Your server's IP address
- **Host:** `blog`
- **Points to:** Your server's IP address
- Repeat for any additional subdomains.
3. **Save DNS Settings:**
- DNS propagation may take up to 24-48 hours, but typically completes within a few hours.
## Reverse Proxy with Nginx
Nginx is configured to route incoming requests to the appropriate frontend services based on the requested domain or subdomain.
### Server Blocks
1. **Primary Domain (`ibrandidit.com`):**
- Routes to the main frontend application (`frontend:3000`).
2. **Shop Subdomain (`shop.ibrandidit.com`):**
- Routes to the shop frontend application (`shop-frontend:3001`).
3. **Blog Subdomain (`blog.ibrandidit.com`):**
- Routes to the blog frontend application (`blog-frontend:3002`).
### SSL/TLS Configuration
SSL certificates are managed using Let's Encrypt via Certbot. Ensure all domains and subdomains have valid SSL certificates for HTTPS support.
## Docker Compose Configuration
The `docker-compose.yml` file orchestrates all services, including the newly added frontend services for the shop and blog subsites.
### Key Additions:
- **Shop Frontend Service (`shop-frontend`):**
- Builds from `./frontend/shop-dashboard`.
- Exposes port `3001` internally.
- **Blog Frontend Service (`blog-frontend`):**
- Builds from `./frontend/blog-dashboard`.
- Exposes port `3002` internally.
- **Nginx Service (`nginx`):**
- Acts as a reverse proxy.
- Exposes ports `80` and `443`.
- Mounts Nginx configuration and SSL certificates.
### Example `docker-compose.yml`:
*(Refer to the [Implementation Steps](#docker-compose-configuration) section for a detailed example.)*
## Frontend Setup
### Primary Frontend (`frontend/user-dashboard`)
- **URL:** `https://ibrandidit.com`
- **Service Name:** `frontend:3000`
### Shop Frontend (`frontend/shop-dashboard`)
- **URL:** `https://shop.ibrandidit.com`
- **Service Name:** `shop-frontend:3001`
### Blog Frontend (`frontend/blog-dashboard`)
- **URL:** `https://blog.ibrandidit.com`
- **Service Name:** `blog-frontend:3002`
#### Setting Up Shop and Blog Frontends
1. **Initialize React Applications:**
```bash
# Shop Frontend
npx create-react-app frontend/shop-dashboard
# Blog Frontend
npx create-react-app frontend/blog-dashboard
2. Customize Each Frontend:
o Shop Frontend:
Tailor the interface for product browsing and purchasing.
Connect to the shop-service backend.
o Blog Frontend:
Design the interface for blog posts and articles.
Connect to the blog-service backend.
3. Environment Variables:
o Shop Frontend (frontend/shop-dashboard/.env):
env
Copy code
REACT_APP_API_BASE_URL=http://order-service:5004
# Add other environment variables specific to the shop
o Blog Frontend (frontend/blog-dashboard/.env):
env
Copy code
REACT_APP_API_BASE_URL=http://blog-service:5010
# Add other environment variables specific to the blog
4. Build Frontend Applications:
bash
Copy code
cd frontend/shop-dashboard
npm run build
cd frontend/blog-dashboard
npm run build
Backend Services Setup
Shop Backend (shop-service)
1. Create Service Directory:
bash
Copy code
mkdir shop-service
cd shop-service
npm init -y
2. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors
npm install --save-dev nodemon eslint prettier jest supertest
3. Project Structure:
bash
Copy code
shop-service/
├── controllers/
│ └── productController.js
├── models/
│ └── Product.js
├── routes/
│ └── productRoutes.js
├── middleware/
│ └── authenticate.js
├── tests/
│ └── productController.test.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
4. Sample Code:
o models/Product.js:
javascript
Copy code
// shop-service/models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
},
stock: {
type: Number,
required: true,
default: 0,
},
image: {
type: String,
},
category: {
type: String,
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Product', ProductSchema);
o controllers/productController.js:
javascript
Copy code
// shop-service/controllers/productController.js
const Product = require('../models/Product');
// Get All Products
const getAllProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json({ products });
} catch (error) {
console.error('Error fetching products:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get Single Product
const getProductById = async (req, res) => {
const { id } = req.params;
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
res.status(200).json({ product });
} catch (error) {
console.error('Error fetching product:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Create Product
const createProduct = async (req, res) => {
const { name, description, price, stock, image, category } = req.body;
try {
const product = new Product({
name,
description,
price,
stock,
image,
category,
});
await product.save();
res.status(201).json({ message: 'Product created successfully', product });
} catch (error) {
console.error('Error creating product:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Update Product
const updateProduct = async (req, res) => {
const { id } = req.params;
const { name, description, price, stock, image, category } = req.body;
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
product.name = name || product.name;
product.description = description || product.description;
product.price = price || product.price;
product.stock = stock || product.stock;
product.image = image || product.image;
product.category = category || product.category;
await product.save();
res.status(200).json({ message: 'Product updated successfully', product });
} catch (error) {
console.error('Error updating product:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Delete Product
const deleteProduct = async (req, res) => {
const { id } = req.params;
try {
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
await product.remove();
res.status(200).json({ message: 'Product deleted successfully' });
} catch (error) {
console.error('Error deleting product:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
getAllProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct,
};
o routes/productRoutes.js:
javascript
Copy code
// shop-service/routes/productRoutes.js
const express = require('express');
const {
getAllProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct,
} = require('../controllers/productController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Public Routes
router.get('/', getAllProducts);
router.get('/:id', getProductById);
// Protected Routes
router.post('/', authenticate, createProduct);
router.put('/:id', authenticate, updateProduct);
router.delete('/:id', authenticate, deleteProduct);
module.exports = router;
o middleware/authenticate.js:
(Same as in previous stages)
javascript
Copy code
// shop-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
o index.js:
javascript
Copy code
// shop-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const productRoutes = require('./routes/productRoutes');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/products', productRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Shop Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5011;
app.listen(PORT, () => {
logger.info(`Shop Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
o utils/logger.js:
javascript
Copy code
// shop-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'shop-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
o tests/productController.test.js:
javascript
Copy code
// shop-service/tests/productController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Product = require('../models/Product');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Product API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Product.deleteMany();
});
describe('GET /api/products', () => {
it('should fetch all products', async () => {
await Product.create([
{ name: 'Product 1', description: 'Desc 1', price: 10, stock: 100 },
{ name: 'Product 2', description: 'Desc 2', price: 20, stock: 200 },
]);
const res = await request(app).get('/api/products').send();
expect(res.statusCode).toEqual(200);
expect(res.body.products).toHaveLength(2);
});
});
describe('POST /api/products', () => {
it('should create a new product', async () => {
const res = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${token}`)
.send({
name: 'Product 3',
description: 'Desc 3',
price: 30,
stock: 300,
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Product created successfully');
expect(res.body.product).toHaveProperty('name', 'Product 3');
});
it('should not allow unauthenticated product creation', async () => {
const res = await request(app).post('/api/products').send({
name: 'Product 4',
description: 'Desc 4',
price: 40,
stock: 400,
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
// Additional tests for update, delete, etc., can be implemented similarly
});
o Run Backend Tests:
bash
Copy code
cd shop-service
npm test
Blog Backend (blog-service)
1. Create Service Directory:
bash
Copy code
mkdir blog-service
cd blog-service
npm init -y
2. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors
npm install --save-dev nodemon eslint prettier jest supertest
3. Project Structure:
bash
Copy code
blog-service/
├── controllers/
│ └── postController.js
├── models/
│ └── Post.js
├── routes/
│ └── postRoutes.js
├── middleware/
│ └── authenticate.js
├── tests/
│ └── postController.test.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
4. Sample Code:
o models/Post.js:
javascript
Copy code
// blog-service/models/Post.js
const mongoose = require('mongoose');
const PostSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
tags: [
{
type: String,
},
],
image: {
type: String,
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Post', PostSchema);
o controllers/postController.js:
javascript
Copy code
// blog-service/controllers/postController.js
const Post = require('../models/Post');
// Get All Posts
const getAllPosts = async (req, res) => {
try {
const posts = await Post.find().populate('author', 'name email');
res.status(200).json({ posts });
} catch (error) {
console.error('Error fetching posts:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get Single Post
const getPostById = async (req, res) => {
const { id } = req.params;
try {
const post = await Post.findById(id).populate('author', 'name email');
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
res.status(200).json({ post });
} catch (error) {
console.error('Error fetching post:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Create Post
const createPost = async (req, res) => {
const { title, content, tags, image } = req.body;
const authorId = req.user.id;
try {
const post = new Post({
title,
content,
author: authorId,
tags,
image,
});
await post.save();
res.status(201).json({ message: 'Post created successfully', post });
} catch (error) {
console.error('Error creating post:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Update Post
const updatePost = async (req, res) => {
const { id } = req.params;
const { title, content, tags, image } = req.body;
const authorId = req.user.id;
try {
const post = await Post.findById(id);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
if (post.author.toString() !== authorId) {
return res.status(403).json({ message: 'Unauthorized to update this post' });
}
post.title = title || post.title;
post.content = content || post.content;
post.tags = tags || post.tags;
post.image = image || post.image;
await post.save();
res.status(200).json({ message: 'Post updated successfully', post });
} catch (error) {
console.error('Error updating post:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Delete Post
const deletePost = async (req, res) => {
const { id } = req.params;
const authorId = req.user.id;
try {
const post = await Post.findById(id);
if (!post) {
return res.status(404).json({ message: 'Post not found' });
}
if (post.author.toString() !== authorId) {
return res.status(403).json({ message: 'Unauthorized to delete this post' });
}
await post.remove();
res.status(200).json({ message: 'Post deleted successfully' });
} catch (error) {
console.error('Error deleting post:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
getAllPosts,
getPostById,
createPost,
updatePost,
deletePost,
};
o routes/postRoutes.js:
javascript
Copy code
// blog-service/routes/postRoutes.js
const express = require('express');
const {
getAllPosts,
getPostById,
createPost,
updatePost,
deletePost,
} = require('../controllers/postController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Public Routes
router.get('/', getAllPosts);
router.get('/:id', getPostById);
// Protected Routes
router.post('/', authenticate, createPost);
router.put('/:id', authenticate, updatePost);
router.delete('/:id', authenticate, deletePost);
module.exports = router;
o index.js:
javascript
Copy code
// blog-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const postRoutes = require('./routes/postRoutes');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/posts', postRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Blog Service API');
});
// Error Handling Middleware
app.use((err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
});
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5010;
app.listen(PORT, () => {
logger.info(`Blog Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
o utils/logger.js:
javascript
Copy code
// blog-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'blog-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
o tests/postController.test.js:
javascript
Copy code
// blog-service/tests/postController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Post = require('../models/Post');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Post API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Post.deleteMany();
});
describe('GET /api/posts', () => {
it('should fetch all posts', async () => {
await Post.create([
{
title: 'Post 1',
content: 'Content 1',
author: userPayload.id,
},
{
title: 'Post 2',
content: 'Content 2',
author: userPayload.id,
},
]);
const res = await request(app).get('/api/posts').send();
expect(res.statusCode).toEqual(200);
expect(res.body.posts).toHaveLength(2);
});
});
describe('POST /api/posts', () => {
it('should create a new post', async () => {
const res = await request(app)
.post('/api/posts')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Post 3',
content: 'Content 3',
tags: ['tag1', 'tag2'],
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Post created successfully');
expect(res.body.post).toHaveProperty('title', 'Post 3');
});
it('should not allow unauthenticated post creation', async () => {
const res = await request(app).post('/api/posts').send({
title: 'Post 4',
content: 'Content 4',
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
// Additional tests for update, delete, etc., can be implemented similarly
});
o Run Backend Tests:
bash
Copy code
cd blog-service
npm test
3.9. Updating Nginx Configuration
Ensure that Nginx routes traffic correctly to each frontend service based on the domain or subdomain.
1. Primary Domain (ibrandidit.com):
o Proxy Pass: http://frontend:3000
2. Shop Subdomain (shop.ibrandidit.com):
o Proxy Pass: http://shop-frontend:3001
3. Blog Subdomain (blog.ibrandidit.com):
o Proxy Pass: http://blog-frontend:3002
4. Reload Nginx to Apply Changes:
bash
Copy code
sudo systemctl reload nginx
3.10. Finalizing Docker Compose for Frontend Services
Ensure that the frontend/shop-dashboard and frontend/blog-dashboard directories contain valid Dockerfiles.
1. Shop Frontend Dockerfile (frontend/shop-dashboard/Dockerfile):
dockerfile
Copy code
# frontend/shop-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve
RUN npm install -g serve
# Expose port
EXPOSE 3001
# Start the app
CMD ["serve", "-s", "build", "-l", "3001"]
2. Blog Frontend Dockerfile (frontend/blog-dashboard/Dockerfile):
dockerfile
Copy code
# frontend/blog-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve
RUN npm install -g serve
# Expose port
EXPOSE 3002
# Start the app
CMD ["serve", "-s", "build", "-l", "3002"]
3. Rebuild and Restart Docker Containers:
bash
Copy code
docker-compose up --build -d
o Ensure all services are up and running:
bash
Copy code
docker-compose ps
4. Testing the Integration
4.1. Accessing Domains and Subdomains
1. Primary Domain:
o URL: https://ibrandidit.com
o Expected Behavior: Loads the main user dashboard frontend.
2. Shop Subdomain:
o URL: https://shop.ibrandidit.com
o Expected Behavior: Loads the shop frontend application.
3. Blog Subdomain:
o URL: https://blog.ibrandidit.com
o Expected Behavior: Loads the blog frontend application.
4.2. Verifying SSL Certificates
• Ensure that all domains and subdomains are accessible over HTTPS without any certificate warnings.
• Check certificate details by clicking the padlock icon in the browser's address bar.
4.3. API Communication
1. Primary Frontend:
o Interacts with existing backend services (Auth, Order, etc.).
o Ensure all features work as expected.
2. Shop Frontend:
o Interacts with the shop-service.
o Test product browsing, purchasing, and other shop-specific functionalities.
3. Blog Frontend:
o Interacts with the blog-service.
o Test creating, viewing, updating, and deleting blog posts.
4.4. Load Testing (Optional)
• Use tools like Apache JMeter or k6 to perform load testing on your domains and subsites to ensure they can handle expected traffic.
4.5. Error Handling
• Accessing Non-Existent Subdomain:
o URL: https://unknown.ibrandidit.com
o Expected Behavior: Nginx should return a 404 Not Found or a custom error page.
• Invalid API Requests:
o Test how each frontend handles API errors, such as failed authentication or invalid data submissions.
5. Documentation and Usage
5.1. Running the Domain & Subsite Integration Locally
1. Ensure All Services Are Running:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
2. Access Frontends:
o Primary Frontend: https://ibrandidit.com
o Shop Frontend: https://shop.ibrandidit.com
o Blog Frontend: https://blog.ibrandidit.com
5.2. Interacting with Each Subsite
1. Primary Frontend (ibrandidit.com):
• Features:
o User Dashboard
o Profile Management
o Order History
o Affiliate & Distributor Management
o AI Features (Chatbot, Image Generator)
o Product & Template Customization
2. Shop Frontend (shop.ibrandidit.com):
• Features:
o Product Browsing
o Shopping Cart
o Checkout Process
o Order Tracking
3. Blog Frontend (blog.ibrandidit.com):
• Features:
o Create and Manage Blog Posts
o View and Read Articles
o Commenting (if implemented)
o Tagging and Categorization
5.3. API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Primary Services:
o Auth Service: http://ibrandidit.com/api-docs
o Order Service: http://ibrandidit.com/api-docs
o Payment Service: http://ibrandidit.com/api-docs
o Shipping Service: http://ibrandidit.com/api-docs
o AI Service: http://ibrandidit.com/api-docs
o Product Customization Service: http://ibrandidit.com/api-docs
o Affiliate & Distributor Management Service: http://ibrandidit.com/api-docs
o Inventory Management Service: http://ibrandidit.com/api-docs
• Subsite Services:
o Shop Service: http://shop.ibrandidit.com/api-docs
o Blog Service: http://blog.ibrandidit.com/api-docs
5.4. Testing
Backend Testing
1. Run All Backend Service Tests:
bash
Copy code
# For each backend service, run:
cd path/to/service
npm test
o Ensure all tests pass, verifying the functionality of each API endpoint.
Frontend Testing
1. Run All Frontend Tests:
bash
Copy code
# For each frontend application, run:
cd path/to/frontend
npm test
o Ensure that all tests pass, especially those related to domain-specific functionalities.
5.5. Linting and Formatting
Maintain code quality and consistency using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd path/to/service-or-frontend
npm run lint
2. Run Formatting:
bash
Copy code
cd path/to/service-or-frontend
npm run format
5.6. Deployment
1. Ensure All Services Are Containerized:
o All backend and frontend services should have Dockerfiles.
2. Build and Deploy with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
3. Verify Deployment:
o Access each domain and subdomain to ensure they're operational and serving the correct frontend applications.
4. Monitor Services:
o Use Docker logs to monitor service health.
bash
Copy code
docker-compose logs -f
5.7. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/domain-integration
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add Nginx reverse proxy configuration for domain integration"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/domain-integration
o Submit a pull request from your fork to the main repository for review.
6. Additional Configuration and Best Practices
6.1. Security Enhancements
1. Firewall Configuration:
o Allow Only Necessary Ports:
bash
Copy code
sudo ufw allow 'Nginx Full'
sudo ufw enable
2. Implement HTTP Strict Transport Security (HSTS):
o Add the following line to your Nginx server blocks (within the SSL configuration):
nginx
Copy code
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
3. Disable Unused Nginx Modules:
o Ensure that only necessary Nginx modules are enabled to minimize security risks.
6.2. Performance Optimization
1. Enable Gzip Compression:
o Add the following lines to your Nginx server blocks:
nginx
Copy code
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
2. Implement Caching:
o Use Nginx's caching capabilities to cache static assets and reduce load times.
3. Load Balancing:
o If expecting high traffic, configure Nginx to distribute load across multiple instances of frontend or backend services.
6.3. Monitoring and Logging
1. Implement Monitoring Tools:
o Use tools like Prometheus and Grafana for real-time monitoring of service performance.
2. Centralized Logging:
o Aggregate logs from all services using tools like ELK Stack (Elasticsearch, Logstash, Kibana).
6.4. Backup and Recovery
1. Database Backups:
o Schedule regular backups of your MongoDB databases.
o Use tools like mongodump and mongorestore or managed services' backup solutions.
2. Docker Volume Backups:
o Ensure that Docker volumes, especially for MongoDB data, are backed up regularly.
6.5. Version Control Best Practices
1. Commit Regularly:
o Make frequent, small commits with clear messages.
2. Use Feature Branches:
o Develop new features or fixes in separate branches to maintain codebase stability.
3. Code Reviews:
o Implement pull requests and code reviews to ensure code quality and knowledge sharing.
6.6. Documentation Maintenance
1. Update Documentation Regularly:
o Ensure that all changes are reflected in the documentation.
o Encourage team members to contribute to documentation improvements.
2. Use Tools for Documentation:
o Utilize tools like Swagger for API documentation.
o Maintain README files with clear instructions and information.
________________________________________
7. Troubleshooting Tips
1. Nginx Not Routing Correctly:
o Problem: Requests to a domain or subdomain are not reaching the intended frontend service.
o Solution:
Verify that the server blocks in Nginx are correctly configured.
Ensure that the Docker Compose service names match those in the Nginx configuration.
Check Nginx logs for any errors.
2. SSL Certificate Issues:
o Problem: Browsers show certificate warnings or errors.
o Solution:
Ensure that SSL certificates are correctly installed and renewed.
Verify that the certificate paths in the Nginx configuration are accurate.
Use sudo certbot renew to manually renew certificates if automatic renewal fails.
3. DNS Propagation Delays:
o Problem: Newly created domains or subdomains are not resolving.
o Solution:
Wait for DNS changes to propagate (up to 48 hours).
Use tools like WhatsMyDNS to check DNS propagation status.
4. Frontend Services Not Responding:
o Problem: Accessing a domain returns a blank page or 502 Bad Gateway.
o Solution:
Ensure that the frontend services are running and healthy.
Check Docker logs for the respective frontend service.
Verify that the proxy pass URLs in Nginx are correct.
5. Authentication Errors:
o Problem: Users receive 401 Unauthorized errors when accessing APIs from subsites.
o Solution:
Ensure that JWT tokens are correctly passed in the Authorization header.
Verify that the authentication middleware is correctly implemented in backend services.
Check that all services share the same JWT_SECRET.
6. Resource Conflicts:
o Problem: Multiple services are trying to use the same port.
o Solution:
Ensure that each service in Docker Compose has unique port mappings.
Use internal port exposure (expose) and let Nginx handle external port assignments.
7. Firewall Restrictions:
o Problem: Ports 80 and 443 are blocked, preventing HTTP and HTTPS traffic.
o Solution:
Adjust firewall settings to allow incoming traffic on ports 80 and 443.
Example for UFW (Ubuntu):
bash
Copy code
sudo ufw allow 'Nginx Full'
sudo ufw enable
8. Performance Bottlenecks:
o Problem: Subsites are loading slowly.
o Solution:
Enable caching in Nginx.
Optimize frontend assets (minification, image optimization).
Scale services horizontally if necessary.
________________________________________
8. Complete Code and Configuration Files
8.1. Nginx Configuration Files
1. Primary Domain (/etc/nginx/sites-available/ibrandidit.com.conf):
nginx
Copy code
server {
listen 80;
server_name ibrandidit.com www.ibrandidit.com;
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name ibrandidit.com www.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
2. Shop Subdomain (/etc/nginx/sites-available/shop.ibrandidit.com.conf):
nginx
Copy code
server {
listen 80;
server_name shop.ibrandidit.com;
location / {
proxy_pass http://shop-frontend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name shop.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/shop.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://shop-frontend:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
3. Blog Subdomain (/etc/nginx/sites-available/blog.ibrandidit.com.conf):
nginx
Copy code
server {
listen 80;
server_name blog.ibrandidit.com;
location / {
proxy_pass http://blog-frontend:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name blog.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/blog.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://blog-frontend:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
8.2. Docker Compose File
docker-compose.yml:
yaml
Copy code
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
auth-service:
build: ./auth-service
restart: always
ports:
- "5003:5003"
environment:
- PORT=5003
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/authdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- GOOGLE_CLIENT_ID=your_google_client_id
- GOOGLE_CLIENT_SECRET=your_google_client_secret
- GOOGLE_CALLBACK_URL=http://auth-service:5003/auth/google/callback
- FACEBOOK_APP_ID=your_facebook_app_id
- FACEBOOK_APP_SECRET=your_facebook_app_secret
- FACEBOOK_CALLBACK_URL=http://auth-service:5003/auth/facebook/callback
- MAILCHIMP_API_KEY=your_mailchimp_api_key
- MAILCHIMP_SERVER_PREFIX=your_server_prefix
- MAILCHIMP_LIST_ID=your_mailchimp_list_id
depends_on:
- mongodb
- rabbitmq
order-service:
build: ./order-service
restart: always
ports:
- "5004:5004"
environment:
- PORT=5004
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/orderdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
payment-service:
build: ./payment-service
restart: always
ports:
- "5005:5005"
environment:
- PORT=5005
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/paymentdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
- STRIPE_SECRET_KEY=your_stripe_secret_key
depends_on:
- mongodb
- rabbitmq
shipping-service:
build: ./shipping-service
restart: always
ports:
- "5006:5006"
environment:
- PORT=5006
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shippingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
- SHIPPO_API_TOKEN=your_shippo_api_token
depends_on:
- mongodb
- rabbitmq
ai-service:
build: ./ai-service
restart: always
ports:
- "5007:5007"
environment:
- PORT=5007
- NODE_ENV=development
- OPENAI_API_KEY=your_openai_api_key
depends_on:
- mongodb
- rabbitmq
product-customization-service:
build: ./product-customization-service
restart: always
ports:
- "5008:5008"
environment:
- PORT=5008
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/productdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
affiliate-distributor-service:
build: ./affiliate-distributor-service
restart: always
ports:
- "5009:5009"
environment:
- PORT=5009
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/affiliatedb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
shop-service:
build: ./shop-service
restart: always
ports:
- "5011:5011"
environment:
- PORT=5011
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/shopdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
blog-service:
build: ./blog-service
restart: always
ports:
- "5010:5010"
environment:
- PORT=5010
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/blogdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
expose:
- "3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
- REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
- REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
- affiliate-distributor-service
shop-frontend:
build: ./frontend/shop-dashboard
restart: always
expose:
- "3001"
environment:
- REACT_APP_API_BASE_URL=http://shop-service:5011
# Add other environment variables specific to shop
depends_on:
- auth-service
- order-service
# ... other dependencies
blog-frontend:
build: ./frontend/blog-dashboard
restart: always
expose:
- "3002"
environment:
- REACT_APP_API_BASE_URL=http://blog-service:5010
# Add other environment variables specific to blog
depends_on:
- auth-service
- blog-service
# ... other dependencies
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/sites-available:/etc/nginx/sites-available
- ./nginx/sites-enabled:/etc/nginx/sites-enabled
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- shop-frontend
- blog-frontend
# ... other services
volumes:
mongo-data:
Notes:
• Volume Mounts for Nginx:
o Ensure that the Nginx configuration files are correctly placed in ./nginx/sites-available and symlinked to ./nginx/sites-enabled.
o SSL certificates from Let's Encrypt are stored in /etc/letsencrypt.
• Service Names:
o Ensure that service names in Docker Compose (frontend, shop-frontend, blog-frontend) match the proxy pass URLs in Nginx configuration.
8.3. Frontend Components
1. Primary Frontend (frontend/user-dashboard):
o URL: https://ibrandidit.com
o Functionality: User dashboard, profile management, order history, affiliate & distributor management, AI features, product & template customization.
2. Shop Frontend (frontend/shop-dashboard):
o URL: https://shop.ibrandidit.com
o Functionality: Product browsing, shopping cart, checkout process, order tracking.
3. Blog Frontend (frontend/blog-dashboard):
o URL: https://blog.ibrandidit.com
o Functionality: Creating, viewing, updating, and deleting blog posts.
Ensure that each frontend application communicates with its respective backend service via the configured API base URLs.
8.4. Backend Services
1. Primary Backend Services:
o Auth Service: Handles user authentication and authorization.
o Order Service: Manages orders and transactions.
o Payment Service: Processes payments.
o Shipping Service: Handles shipping and logistics.
o AI Service: Provides AI-powered features.
o Product Customization Service: Manages product customization.
o Affiliate & Distributor Management Service: Manages affiliates and distributors.
o Inventory Management Service: Manages product inventory.
2. Subsite Backend Services:
o Shop Service (shop-service): Manages shop-specific APIs and functionalities.
o Blog Service (blog-service): Manages blog-specific APIs and functionalities.
Ensure that all backend services are properly connected to MongoDB and RabbitMQ, and that they communicate seamlessly with frontend applications.
________________________________________
9. Complete Code and Configuration Files
9.1. Nginx Configuration Files
(Already provided in the Nginx Configuration Files section.)
9.2. Docker Compose File
(Already provided in the Docker Compose File section.)
9.3. Frontend Applications
1. Primary Frontend (frontend/user-dashboard):
o Ensure that the React application is properly set up and communicates with backend services via environment variables.
o Implement necessary components for user dashboard and integrated features.
2. Shop Frontend (frontend/shop-dashboard):
o Implement product browsing, shopping cart, and checkout functionalities.
o Connect to shop-service APIs for product data and order processing.
3. Blog Frontend (frontend/blog-dashboard):
o Implement blog post creation, viewing, editing, and deletion functionalities.
o Connect to blog-service APIs for managing blog content.
9.4. Backend Services
1. Shop Service (shop-service):
o Models, Controllers, Routes, Middleware, Tests: (Already provided above.)
o Ensure that the service handles product-related operations effectively.
2. Blog Service (blog-service):
o Models, Controllers, Routes, Middleware, Tests: (Already provided above.)
o Ensure that the service handles blog-related operations effectively.
9.5. Testing Frontend Components
1. Primary Frontend Tests:
o Implement tests for user dashboard functionalities, ensuring seamless integration with backend services.
2. Shop Frontend Tests:
o Implement tests for product browsing, cart management, and checkout processes.
3. Blog Frontend Tests:
o Implement tests for blog post management features.
(Sample tests for Affiliate & Distributor components were provided in Stage 17. Similar approaches can be adopted for shop and blog frontend components.)
________________________________________
10. Troubleshooting Tips
1. Nginx Not Routing Correctly:
o Problem: Requests to a domain or subdomain are not reaching the intended frontend service.
o Solution:
Verify that the server blocks in Nginx are correctly configured.
Ensure that the Docker Compose service names match those in the Nginx configuration.
Check Nginx logs for any errors:
bash
Copy code
sudo tail -f /var/log/nginx/error.log
2. SSL Certificate Issues:
o Problem: Browsers show certificate warnings or errors.
o Solution:
Ensure that SSL certificates are correctly installed and renewed.
Verify that the certificate paths in the Nginx configuration are accurate.
Use sudo certbot renew to manually renew certificates if automatic renewal fails.
3. DNS Propagation Delays:
o Problem: Newly created domains or subdomains are not resolving.
o Solution:
Wait for DNS changes to propagate (up to 48 hours).
Use tools like WhatsMyDNS to check DNS propagation status.
4. Frontend Services Not Responding:
o Problem: Accessing a domain returns a blank page or 502 Bad Gateway.
o Solution:
Ensure that the frontend services are running and healthy.
Check Docker logs for the respective frontend service:
bash
Copy code
docker-compose logs frontend
docker-compose logs shop-frontend
docker-compose logs blog-frontend
Verify that the proxy pass URLs in Nginx are correct.
5. Authentication Errors:
o Problem: Users receive 401 Unauthorized errors when accessing APIs from subsites.
o Solution:
Ensure that JWT tokens are correctly passed in the Authorization header.
Verify that the authentication middleware is correctly implemented in backend services.
Check that all services share the same JWT_SECRET.
6. Database Connection Issues:
o Problem: Services cannot connect to MongoDB.
o Solution:
Ensure that MongoDB is running and accessible.
Verify the MONGODB_URI in the .env files.
Check network configurations and firewall settings.
7. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
8. Resource Conflicts:
o Problem: Multiple services are trying to use the same port.
o Solution:
Ensure that each service in Docker Compose has unique port mappings.
Use internal port exposure (expose) and let Nginx handle external port assignments.
9. Form Validation Errors:
o Problem: Users cannot submit forms on subsites due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
10. Performance Bottlenecks:
o Problem: Subsites are loading slowly.
o Solution:
Enable caching in Nginx.
Optimize frontend assets (minification, image optimization).
Scale services horizontally if necessary.
________________________________________
11. Additional Resources
• Nginx Documentation: https://nginx.org/en/docs/
• Docker Documentation: https://docs.docker.com/get-started/
• Docker Compose Documentation: https://docs.docker.com/compose/
• Let's Encrypt Documentation: https://letsencrypt.org/docs/
• Certbot Documentation: https://certbot.eff.org/docs/
• React.js Documentation: https://reactjs.org/docs/getting-started.html
• Express.js Documentation: https://expressjs.com/en/4x/api.html
• Mongoose Documentation: https://mongoosejs.com/docs/guide.html
• JWT Documentation: https://jwt.io/introduction/
• Winston Documentation: https://github.com/winstonjs/winston
• React Testing Library Documentation: https://testing-library.com/docs/react-testing-library/intro/
• Jest Documentation: https://jestjs.io/docs/getting-started
• Form Validation with Yup: https://github.com/jquense/yup
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 18: Domain & Subsite Integration within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced load balancing strategies, integrating with CDN services for faster content delivery, or enhancing security measures.
________________________________________
By following this comprehensive guide for Stage 18: Domain & Subsite Integration, you've successfully extended your eCommerce platform's capabilities to manage multiple domains and subsites efficiently. This setup not only enhances the organization's structure but also provides a scalable foundation for future expansions and targeted marketing strategies.
Next Steps:
Proceed to Stage 19: Customer Support Integration to further enhance your platform by incorporating robust customer support functionalities, ensuring excellent user satisfaction and engagement.
Stage 19: Fundraising & Quick Store Setup is a pivotal phase in enhancing your eCommerce platform, allowing users to engage in fundraising activities and set up their own stores with ease. This stage introduces functionalities for creating and managing fundraising campaigns and provides tools for users to establish their online storefronts swiftly. Below is a comprehensive guide tailored for beginners, complete with code, dependencies, configuration files, and documentation to ensure seamless integration.
________________________________________
Stage 19: Fundraising & Quick Store Setup
________________________________________
1. Overview
Objective:
• Fundraising Features:
o Allow users to create, manage, and promote fundraising campaigns.
o Enable donations via PayPal integration.
o Track campaign progress and display real-time updates.
• Quick Store Setup:
o Provide users with tools to set up their online stores rapidly.
o Offer customizable templates and easy product uploads.
o Integrate with existing services for seamless operations.
Benefits:
• Monetization Opportunities: Users can raise funds for personal or business initiatives.
• User Empowerment: Simplifies the process of setting up an online store, encouraging more users to participate.
• Enhanced Engagement: Engaging features like fundraising campaigns increase user interaction and retention.
• Scalability: Facilitates platform growth by enabling users to expand their eCommerce presence effortlessly.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js
• Frontend Framework: React.js
• Database: MongoDB – Using Mongoose ODM
• Authentication: JWT (JSON Web Tokens) – Existing setup
• Payment Integration: PayPal REST APIs
• Containerization: Docker
• Orchestration: Docker Compose
• API Documentation: Swagger
• Testing Frameworks: Jest and Supertest
• Logging: Winston
• Error Handling: Centralized middleware
• Environment Variables Management: dotenv
• Version Control: Git
________________________________________
3. Implementation Steps
3.1. Backend Implementation
3.1.1. Create Fundraising Service
1. Directory Structure:
bash
Copy code
fundraising-service/
├── controllers/
│ └── campaignController.js
├── models/
│ └── Campaign.js
├── routes/
│ └── campaignRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ └── campaignController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
2. Initialize the Project:
bash
Copy code
mkdir fundraising-service
cd fundraising-service
npm init -y
3. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors helmet express-rate-limit winston paypal-rest-sdk
npm install --save-dev nodemon eslint prettier jest supertest
4. Configuration Files:
o .env File:
env
Copy code
PORT=5012
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/fundraisingdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
PAYPAL_MODE=sandbox
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
o package.json Scripts:
json
Copy code
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"lint": "eslint .",
"format": "prettier --write .",
"test": "jest"
}
}
5. Implementing the Campaign Model:
o models/Campaign.js:
javascript
Copy code
// fundraising-service/models/Campaign.js
const mongoose = require('mongoose');
const CampaignSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
goalAmount: {
type: Number,
required: true,
},
currentAmount: {
type: Number,
default: 0,
},
image: {
type: String,
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
status: {
type: String,
enum: ['active', 'completed', 'cancelled'],
default: 'active',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('Campaign', CampaignSchema);
6. Implementing the Campaign Controller:
o controllers/campaignController.js:
javascript
Copy code
// fundraising-service/controllers/campaignController.js
const Campaign = require('../models/Campaign');
const paypal = require('paypal-rest-sdk');
// Configure PayPal SDK
paypal.configure({
mode: process.env.PAYPAL_MODE, // 'sandbox' or 'live'
client_id: process.env.PAYPAL_CLIENT_ID,
client_secret: process.env.PAYPAL_CLIENT_SECRET,
});
// Create a new campaign
const createCampaign = async (req, res) => {
const { title, description, goalAmount, image } = req.body;
const creatorId = req.user.id;
try {
const campaign = new Campaign({
title,
description,
goalAmount,
image,
creator: creatorId,
});
await campaign.save();
res.status(201).json({ message: 'Campaign created successfully', campaign });
} catch (error) {
console.error('Error creating campaign:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get all campaigns
const getAllCampaigns = async (req, res) => {
try {
const campaigns = await Campaign.find().populate('creator', 'name email');
res.status(200).json({ campaigns });
} catch (error) {
console.error('Error fetching campaigns:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get campaign by ID
const getCampaignById = async (req, res) => {
const { id } = req.params;
try {
const campaign = await Campaign.findById(id).populate('creator', 'name email');
if (!campaign) {
return res.status(404).json({ message: 'Campaign not found' });
}
res.status(200).json({ campaign });
} catch (error) {
console.error('Error fetching campaign:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Donate to a campaign
const donateToCampaign = async (req, res) => {
const { id } = req.params;
const { amount } = req.body;
try {
const campaign = await Campaign.findById(id);
if (!campaign) {
return res.status(404).json({ message: 'Campaign not found' });
}
// Create PayPal payment
const create_payment_json = {
intent: 'sale',
payer: {
payment_method: 'paypal',
},
redirect_urls: {
return_url: `http://localhost:5012/campaigns/${id}/success`,
cancel_url: `http://localhost:5012/campaigns/${id}/cancel`,
},
transactions: [
{
item_list: {
items: [
{
name: campaign.title,
sku: '001',
price: amount.toFixed(2),
currency: 'USD',
quantity: 1,
},
],
},
amount: {
currency: 'USD',
total: amount.toFixed(2),
},
description: campaign.description,
},
],
};
paypal.payment.create(create_payment_json, function (error, payment) {
if (error) {
console.error(error.response);
throw error;
} else {
// Find approval URL
for (let link of payment.links) {
if (link.rel === 'approval_url') {
return res.status(200).json({ approvalUrl: link.href });
}
}
res.status(500).json({ message: 'No approval URL found' });
}
});
} catch (error) {
console.error('Error donating to campaign:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Execute PayPal payment
const executePayment = async (req, res) => {
const { id, payerId } = req.query;
const { campaignId } = req.params;
try {
const campaign = await Campaign.findById(campaignId);
if (!campaign) {
return res.status(404).json({ message: 'Campaign not found' });
}
const execute_payment_json = {
payer_id: payerId,
};
paypal.payment.execute(id, execute_payment_json, async function (error, payment) {
if (error) {
console.error(error.response);
return res.status(500).json({ message: 'Payment execution failed' });
} else {
// Update campaign's currentAmount
const donationAmount = parseFloat(payment.transactions[0].amount.total);
campaign.currentAmount += donationAmount;
if (campaign.currentAmount >= campaign.goalAmount) {
campaign.status = 'completed';
}
await campaign.save();
res.status(200).json({ message: 'Donation successful', campaign });
}
});
} catch (error) {
console.error('Error executing payment:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
createCampaign,
getAllCampaigns,
getCampaignById,
donateToCampaign,
executePayment,
};
7. Implementing the Campaign Routes:
o routes/campaignRoutes.js:
javascript
Copy code
// fundraising-service/routes/campaignRoutes.js
const express = require('express');
const {
createCampaign,
getAllCampaigns,
getCampaignById,
donateToCampaign,
executePayment,
} = require('../controllers/campaignController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Public Routes
router.get('/', getAllCampaigns);
router.get('/:id', getCampaignById);
router.get('/:id/success', executePayment);
router.get('/:id/cancel', (req, res) => {
res.status(200).json({ message: 'Payment cancelled' });
});
// Protected Routes
router.post('/', authenticate, createCampaign);
router.post('/:id/donate', authenticate, donateToCampaign);
module.exports = router;
8. Authentication Middleware:
o middleware/authenticate.js:
(Assuming similar to previous stages)
javascript
Copy code
// fundraising-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
9. Error Handling Middleware:
o middleware/errorHandler.js:
javascript
Copy code
// fundraising-service/middleware/errorHandler.js
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
};
module.exports = errorHandler;
10. Logger Utility:
o utils/logger.js:
javascript
Copy code
// fundraising-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'fundraising-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
11. Main Application File:
o index.js:
javascript
Copy code
// fundraising-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const campaignRoutes = require('./routes/campaignRoutes');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/campaigns', campaignRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Fundraising Service API');
});
// Error Handling Middleware
app.use(errorHandler);
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5012;
app.listen(PORT, () => {
logger.info(`Fundraising Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
12. Testing the Campaign Controller:
o tests/campaignController.test.js:
javascript
Copy code
// fundraising-service/tests/campaignController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Campaign = require('../models/Campaign');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Campaign API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Campaign.deleteMany();
});
describe('POST /api/campaigns', () => {
it('should create a new campaign', async () => {
const res = await request(app)
.post('/api/campaigns')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Save the Rainforest',
description: 'A campaign to save the rainforest.',
goalAmount: 5000,
image: 'http://example.com/image.jpg',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Campaign created successfully');
expect(res.body.campaign).toHaveProperty('title', 'Save the Rainforest');
});
it('should not allow unauthenticated campaign creation', async () => {
const res = await request(app).post('/api/campaigns').send({
title: 'Save the Ocean',
description: 'A campaign to save the ocean.',
goalAmount: 3000,
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
describe('GET /api/campaigns', () => {
it('should fetch all campaigns', async () => {
await Campaign.create([
{
title: 'Campaign 1',
description: 'Description 1',
goalAmount: 1000,
creator: userPayload.id,
},
{
title: 'Campaign 2',
description: 'Description 2',
goalAmount: 2000,
creator: userPayload.id,
},
]);
const res = await request(app).get('/api/campaigns').send();
expect(res.statusCode).toEqual(200);
expect(res.body.campaigns).toHaveLength(2);
});
});
describe('GET /api/campaigns/:id', () => {
it('should fetch a single campaign by ID', async () => {
const campaign = await Campaign.create({
title: 'Campaign 3',
description: 'Description 3',
goalAmount: 1500,
creator: userPayload.id,
});
const res = await request(app).get(`/api/campaigns/${campaign._id}`).send();
expect(res.statusCode).toEqual(200);
expect(res.body.campaign).toHaveProperty('title', 'Campaign 3');
});
it('should return 404 if campaign not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app).get(`/api/campaigns/${nonExistentId}`).send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Campaign not found');
});
});
describe('POST /api/campaigns/:id/donate', () => {
it('should initiate a donation', async () => {
const campaign = await Campaign.create({
title: 'Campaign 4',
description: 'Description 4',
goalAmount: 2500,
creator: userPayload.id,
});
const res = await request(app)
.post(`/api/campaigns/${campaign._id}/donate`)
.set('Authorization', `Bearer ${token}`)
.send({
amount: 100,
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('approvalUrl');
});
it('should not allow unauthenticated donations', async () => {
const campaign = await Campaign.create({
title: 'Campaign 5',
description: 'Description 5',
goalAmount: 3000,
creator: userPayload.id,
});
const res = await request(app).post(`/api/campaigns/${campaign._id}/donate`).send({
amount: 150,
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
});
13. Updating Docker Compose Configuration:
o Add the Fundraising Service to docker-compose.yml:
yaml
Copy code
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
# ... Other backend services
fundraising-service:
build: ./fundraising-service
restart: always
ports:
- "5012:5012"
environment:
- PORT=5012
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/fundraisingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
depends_on:
- mongodb
- rabbitmq
frontend:
build: ./frontend/user-dashboard
restart: always
expose:
- "3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
- REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
- REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
- affiliate-distributor-service
# ... Other frontend services
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/sites-available:/etc/nginx/sites-available
- ./nginx/sites-enabled:/etc/nginx/sites-enabled
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- shop-frontend
- blog-frontend
- fundraising-service
# ... Other services
volumes:
mongo-data:
14. Integrating PayPal Payment System:
o Ensure Consistent PayPal Integration Across Services:
Reuse the PayPal SDK configuration (paypal-rest-sdk) in the Fundraising Service as done in previous stages.
Verify that PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are correctly set in the .env files.
Ensure that the PayPal credentials used are valid and have appropriate permissions.
3.2. Frontend Implementation
3.2.1. Create Fundraising Frontend Components
1. Directory Structure:
bash
Copy code
frontend/fundraising-dashboard/
├── src/
│ ├── components/
│ │ ├── Campaign/
│ │ │ ├── CampaignForm.js
│ │ │ ├── CampaignList.js
│ │ │ └── CampaignDetails.js
│ │ └── Donation/
│ │ └── DonateButton.js
│ ├── pages/
│ │ ├── CreateCampaign.js
│ │ ├── Campaigns.js
│ │ └── CampaignDetailsPage.js
│ ├── services/
│ │ └── fundraisingApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
2. Initialize the Project:
bash
Copy code
mkdir -p frontend/fundraising-dashboard
cd frontend/fundraising-dashboard
npx create-react-app .
3. Install Dependencies:
bash
Copy code
npm install axios react-router-dom formik yup react-toastify
npm install --save-dev @testing-library/react @testing-library/jest-dom jest
4. Configuration Files:
o .env File:
env
Copy code
REACT_APP_API_BASE_URL=http://fundraising-service:5012/api
o package.json Scripts:
(Already set by Create React App)
5. Implementing the Fundraising API Service:
o src/services/fundraisingApi.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/services/fundraisingApi.js
import axios from 'axios';
const fundraisingApi = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL,
});
// Add a request interceptor to include JWT token
fundraisingApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token'); // Assuming token is stored in localStorage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default fundraisingApi;
6. Implementing Campaign Components:
o src/components/Campaign/CampaignForm.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/components/Campaign/CampaignForm.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography } from '@mui/material';
import fundraisingApi from '../../services/fundraisingApi';
import { toast } from 'react-toastify';
const CampaignSchema = Yup.object().shape({
title: Yup.string().required('Title is required'),
description: Yup.string().required('Description is required'),
goalAmount: Yup.number()
.required('Goal amount is required')
.positive('Goal amount must be positive'),
image: Yup.string().url('Invalid URL').optional(),
});
const CampaignForm = () => {
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
try {
const res = await fundraisingApi.post('/campaigns', values);
toast.success('Campaign created successfully');
resetForm();
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to create campaign');
} finally {
setSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, margin: 'auto', padding: 2 }}>
<Typography variant="h5" gutterBottom>
Create a New Campaign
</Typography>
<Formik
initialValues={{
title: '',
description: '',
goalAmount: '',
image: '',
}}
validationSchema={CampaignSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isSubmitting }) => (
<Form>
<Field
as={TextField}
name="title"
label="Title"
fullWidth
margin="normal"
error={touched.title && Boolean(errors.title)}
helperText={touched.title && errors.title}
/>
<Field
as={TextField}
name="description"
label="Description"
fullWidth
multiline
rows={4}
margin="normal"
error={touched.description && Boolean(errors.description)}
helperText={touched.description && errors.description}
/>
<Field
as={TextField}
name="goalAmount"
label="Goal Amount (USD)"
type="number"
fullWidth
margin="normal"
error={touched.goalAmount && Boolean(errors.goalAmount)}
helperText={touched.goalAmount && errors.goalAmount}
/>
<Field
as={TextField}
name="image"
label="Image URL"
fullWidth
margin="normal"
error={touched.image && Boolean(errors.image)}
helperText={touched.image && errors.image}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 2 }}
>
{isSubmitting ? 'Creating...' : 'Create Campaign'}
</Button>
</Form>
)}
</Formik>
</Box>
);
};
export default CampaignForm;
o src/components/Campaign/CampaignList.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/components/Campaign/CampaignList.js
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardMedia, CardContent, Typography, Button } from '@mui/material';
import { Link } from 'react-router-dom';
import fundraisingApi from '../../services/fundraisingApi';
import { toast } from 'react-toastify';
const CampaignList = () => {
const [campaigns, setCampaigns] = useState([]);
useEffect(() => {
const fetchCampaigns = async () => {
try {
const res = await fundraisingApi.get('/campaigns');
setCampaigns(res.data.campaigns);
} catch (error) {
console.error(error);
toast.error('Failed to fetch campaigns');
}
};
fetchCampaigns();
}, []);
return (
<Grid container spacing={2} sx={{ padding: 2 }}>
{campaigns.map((campaign) => (
<Grid item xs={12} md={4} key={campaign._id}>
<Card>
{campaign.image && (
<CardMedia component="img" height="140" image={campaign.image} alt={campaign.title} />
)}
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{campaign.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{campaign.description.substring(0, 100)}...
</Typography>
<Typography variant="h6" color="text.primary" sx={{ mt: 1 }}>
Goal: ${campaign.goalAmount}
</Typography>
<Typography variant="h6" color="text.primary">
Raised: ${campaign.currentAmount}
</Typography>
<Button
component={Link}
to={`/campaigns/${campaign._id}`}
variant="contained"
color="primary"
sx={{ mt: 2 }}
>
View Details
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};
export default CampaignList;
o src/components/Campaign/CampaignDetails.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/components/Campaign/CampaignDetails.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Box, Typography, Button, CircularProgress } from '@mui/material';
import fundraisingApi from '../../services/fundraisingApi';
import { toast } from 'react-toastify';
const CampaignDetails = () => {
const { id } = useParams();
const [campaign, setCampaign] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchCampaign = async () => {
try {
const res = await fundraisingApi.get(`/campaigns/${id}`);
setCampaign(res.data.campaign);
} catch (error) {
console.error(error);
toast.error('Failed to fetch campaign details');
} finally {
setLoading(false);
}
};
fetchCampaign();
}, [id]);
const handleDonate = async () => {
try {
const res = await fundraisingApi.post(`/campaigns/${id}/donate`, { amount: 50 }); // Example amount
window.location.href = res.data.approvalUrl;
} catch (error) {
console.error(error);
toast.error('Failed to initiate donation');
}
};
if (loading) {
return <CircularProgress />;
}
if (!campaign) {
return null;
}
return (
<Box sx={{ maxWidth: 800, margin: 'auto', padding: 2 }}>
<Typography variant="h4" gutterBottom>
{campaign.title}
</Typography>
{campaign.image && (
<img src={campaign.image} alt={campaign.title} style={{ width: '100%', maxHeight: 400 }} />
)}
<Typography variant="h6" sx={{ mt: 2 }}>
Goal Amount: ${campaign.goalAmount}
</Typography>
<Typography variant="h6">Raised: ${campaign.currentAmount}</Typography>
<Typography variant="body1" sx={{ mt: 2 }}>
{campaign.description}
</Typography>
<Button variant="contained" color="primary" sx={{ mt: 3 }} onClick={handleDonate}>
Donate $50
</Button>
</Box>
);
};
export default CampaignDetails;
o src/components/Donation/DonateButton.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/components/Donation/DonateButton.js
import React from 'react';
import { Button } from '@mui/material';
import fundraisingApi from '../../services/fundraisingApi';
import { toast } from 'react-toastify';
const DonateButton = ({ campaignId, amount }) => {
const handleDonate = async () => {
try {
const res = await fundraisingApi.post(`/campaigns/${campaignId}/donate`, { amount });
window.location.href = res.data.approvalUrl;
} catch (error) {
console.error(error);
toast.error('Failed to initiate donation');
}
};
return (
<Button variant="contained" color="primary" onClick={handleDonate}>
Donate ${amount}
</Button>
);
};
export default DonateButton;
7. Implementing Fundraising Pages:
o src/pages/CreateCampaign.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/pages/CreateCampaign.js
import React from 'react';
import CampaignForm from '../components/Campaign/CampaignForm';
const CreateCampaign = () => {
return <CampaignForm />;
};
export default CreateCampaign;
o src/pages/Campaigns.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/pages/Campaigns.js
import React from 'react';
import CampaignList from '../components/Campaign/CampaignList';
const Campaigns = () => {
return <CampaignList />;
};
export default Campaigns;
o src/pages/CampaignDetailsPage.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/pages/CampaignDetailsPage.js
import React from 'react';
import CampaignDetails from '../components/Campaign/CampaignDetails';
const CampaignDetailsPage = () => {
return <CampaignDetails />;
};
export default CampaignDetailsPage;
8. Updating Routing in App.js:
o src/App.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import CreateCampaign from './pages/CreateCampaign';
import Campaigns from './pages/Campaigns';
import CampaignDetailsPage from './pages/CampaignDetailsPage';
import Navbar from './components/Navbar'; // Assuming a Navbar component exists
const App = () => {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Campaigns />} />
<Route path="/create-campaign" element={<CreateCampaign />} />
<Route path="/campaigns/:id" element={<CampaignDetailsPage />} />
</Routes>
<ToastContainer />
</Router>
);
};
export default App;
9. Implementing the Navbar (Optional):
o src/components/Navbar.js:
javascript
Copy code
// frontend/fundraising-dashboard/src/components/Navbar.js
import React from 'react';
import { AppBar, Toolbar, Typography, Button } from '@mui/material';
import { Link } from 'react-router-dom';
const Navbar = () => {
const handleLogout = () => {
localStorage.removeItem('token');
window.location.reload();
};
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Fundraising Platform
</Typography>
<Button color="inherit" component={Link} to="/">
Campaigns
</Button>
<Button color="inherit" component={Link} to="/create-campaign">
Create Campaign
</Button>
<Button color="inherit" onClick={handleLogout}>
Logout
</Button>
</Toolbar>
</AppBar>
);
};
export default Navbar;
10. Implementing PayPal Integration Consistently:
o Ensure that the PayPal SDK configuration in the Fundraising Service matches the existing setup in other services.
o Reuse environment variables and configurations where applicable to maintain consistency.
11. Testing the Fundraising Service:
o Run Backend Tests:
bash
Copy code
cd fundraising-service
npm test
o Run Frontend Tests:
bash
Copy code
cd frontend/fundraising-dashboard
npm test
(Implement additional tests for frontend components as needed.)
12. Update Docker Compose Configuration:
o Add the Fundraising Frontend to docker-compose.yml:
yaml
Copy code
frontend-fundraising:
build: ./frontend/fundraising-dashboard
restart: always
expose:
- "3003"
environment:
- REACT_APP_API_BASE_URL=http://fundraising-service:5012/api
depends_on:
- fundraising-service
o Update Nginx Configuration:
Create Server Block for Fundraising Subdomain (fundraising.ibrandidit.com):
/etc/nginx/sites-available/fundraising.ibrandidit.com.conf:
nginx
Copy code
server {
listen 80;
server_name fundraising.ibrandidit.com;
location / {
proxy_pass http://frontend-fundraising:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name fundraising.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/fundraising.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fundraising.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://frontend-fundraising:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
Enable the Server Block:
bash
Copy code
sudo ln -s /etc/nginx/sites-available/fundraising.ibrandidit.com.conf /etc/nginx/sites-enabled/
Obtain SSL Certificate for Fundraising Subdomain:
bash
Copy code
sudo certbot --nginx -d fundraising.ibrandidit.com
Reload Nginx:
bash
Copy code
sudo systemctl reload nginx
13. Finalizing Docker Compose for Fundraising Frontend:
o Ensure the Fundraising Frontend Dockerfile is correctly set up.
o frontend/fundraising-dashboard/Dockerfile:
dockerfile
Copy code
# frontend/fundraising-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve
RUN npm install -g serve
# Expose port
EXPOSE 3003
# Start the app
CMD ["serve", "-s", "build", "-l", "3003"]
o Rebuild and Restart Docker Containers:
bash
Copy code
docker-compose up --build -d
Ensure all services are up and running:
bash
Copy code
docker-compose ps
3.3. Frontend: Quick Store Setup
3.3.1. Implementing Quick Store Setup Features
1. Directory Structure:
bash
Copy code
frontend/quick-store-dashboard/
├── src/
│ ├── components/
│ │ ├── Store/
│ │ │ ├── StoreForm.js
│ │ │ ├── StoreList.js
│ │ │ └── StoreDetails.js
│ │ └── Product/
│ │ └── ProductUploader.js
│ ├── pages/
│ │ ├── CreateStore.js
│ │ ├── Stores.js
│ │ └── StoreDetailsPage.js
│ ├── services/
│ │ └── storeApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
2. Initialize the Project:
bash
Copy code
mkdir -p frontend/quick-store-dashboard
cd frontend/quick-store-dashboard
npx create-react-app .
3. Install Dependencies:
bash
Copy code
npm install axios react-router-dom formik yup react-toastify
npm install --save-dev @testing-library/react @testing-library/jest-dom jest
4. Configuration Files:
o .env File:
env
Copy code
REACT_APP_API_BASE_URL=http://store-service:5013/api
o package.json Scripts:
(Already set by Create React App)
5. Implementing the Store API Service:
o src/services/storeApi.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/services/storeApi.js
import axios from 'axios';
const storeApi = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL,
});
// Add a request interceptor to include JWT token
storeApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token'); // Assuming token is stored in localStorage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default storeApi;
6. Implementing Store Components:
o src/components/Store/StoreForm.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/components/Store/StoreForm.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography } from '@mui/material';
import storeApi from '../../services/storeApi';
import { toast } from 'react-toastify';
const StoreSchema = Yup.object().shape({
name: Yup.string().required('Store name is required'),
description: Yup.string().required('Description is required'),
logo: Yup.string().url('Invalid URL').optional(),
theme: Yup.string().required('Theme is required'),
});
const StoreForm = () => {
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
try {
const res = await storeApi.post('/stores', values);
toast.success('Store created successfully');
resetForm();
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to create store');
} finally {
setSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, margin: 'auto', padding: 2 }}>
<Typography variant="h5" gutterBottom>
Create a New Store
</Typography>
<Formik
initialValues={{
name: '',
description: '',
logo: '',
theme: 'light',
}}
validationSchema={StoreSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isSubmitting }) => (
<Form>
<Field
as={TextField}
name="name"
label="Store Name"
fullWidth
margin="normal"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
/>
<Field
as={TextField}
name="description"
label="Description"
fullWidth
multiline
rows={4}
margin="normal"
error={touched.description && Boolean(errors.description)}
helperText={touched.description && errors.description}
/>
<Field
as={TextField}
name="logo"
label="Logo URL"
fullWidth
margin="normal"
error={touched.logo && Boolean(errors.logo)}
helperText={touched.logo && errors.logo}
/>
<Field
as={TextField}
name="theme"
label="Theme"
select
SelectProps={{
native: true,
}}
fullWidth
margin="normal"
error={touched.theme && Boolean(errors.theme)}
helperText={touched.theme && errors.theme}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</Field>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 2 }}
>
{isSubmitting ? 'Creating...' : 'Create Store'}
</Button>
</Form>
)}
</Formik>
</Box>
);
};
export default StoreForm;
o src/components/Store/StoreList.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/components/Store/StoreList.js
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardMedia, CardContent, Typography, Button } from '@mui/material';
import { Link } from 'react-router-dom';
import storeApi from '../../services/storeApi';
import { toast } from 'react-toastify';
const StoreList = () => {
const [stores, setStores] = useState([]);
useEffect(() => {
const fetchStores = async () => {
try {
const res = await storeApi.get('/stores');
setStores(res.data.stores);
} catch (error) {
console.error(error);
toast.error('Failed to fetch stores');
}
};
fetchStores();
}, []);
return (
<Grid container spacing={2} sx={{ padding: 2 }}>
{stores.map((store) => (
<Grid item xs={12} md={4} key={store._id}>
<Card>
{store.logo && (
<CardMedia component="img" height="140" image={store.logo} alt={store.name} />
)}
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{store.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{store.description.substring(0, 100)}...
</Typography>
<Typography variant="h6" color="text.primary" sx={{ mt: 1 }}>
Theme: {store.theme}
</Typography>
<Button
component={Link}
to={`/stores/${store._id}`}
variant="contained"
color="primary"
sx={{ mt: 2 }}
>
View Store
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};
export default StoreList;
o src/components/Store/StoreDetails.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/components/Store/StoreDetails.js
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Box, Typography, Button, CircularProgress } from '@mui/material';
import storeApi from '../../services/storeApi';
import { toast } from 'react-toastify';
const StoreDetails = () => {
const { id } = useParams();
const [store, setStore] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStore = async () => {
try {
const res = await storeApi.get(`/stores/${id}`);
setStore(res.data.store);
} catch (error) {
console.error(error);
toast.error('Failed to fetch store details');
} finally {
setLoading(false);
}
};
fetchStore();
}, [id]);
if (loading) {
return <CircularProgress />;
}
if (!store) {
return null;
}
return (
<Box sx={{ maxWidth: 800, margin: 'auto', padding: 2 }}>
<Typography variant="h4" gutterBottom>
{store.name}
</Typography>
{store.logo && (
<img src={store.logo} alt={store.name} style={{ width: '100%', maxHeight: 400 }} />
)}
<Typography variant="h6" sx={{ mt: 2 }}>
Theme: {store.theme}
</Typography>
<Typography variant="body1" sx={{ mt: 2 }}>
{store.description}
</Typography>
{/* Additional store details and functionalities can be added here */}
</Box>
);
};
export default StoreDetails;
o src/components/Product/ProductUploader.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/components/Product/ProductUploader.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography } from '@mui/material';
import storeApi from '../../services/storeApi';
import { toast } from 'react-toastify';
const ProductSchema = Yup.object().shape({
name: Yup.string().required('Product name is required'),
description: Yup.string().required('Description is required'),
price: Yup.number()
.required('Price is required')
.positive('Price must be positive'),
stock: Yup.number()
.required('Stock is required')
.integer('Stock must be an integer')
.min(0, 'Stock cannot be negative'),
image: Yup.string().url('Invalid URL').optional(),
});
const ProductUploader = ({ storeId }) => {
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
try {
const res = await storeApi.post(`/stores/${storeId}/products`, values);
toast.success('Product added successfully');
resetForm();
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to add product');
} finally {
setSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, margin: 'auto', padding: 2 }}>
<Typography variant="h5" gutterBottom>
Add a New Product
</Typography>
<Formik
initialValues={{
name: '',
description: '',
price: '',
stock: '',
image: '',
}}
validationSchema={ProductSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isSubmitting }) => (
<Form>
<Field
as={TextField}
name="name"
label="Product Name"
fullWidth
margin="normal"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
/>
<Field
as={TextField}
name="description"
label="Description"
fullWidth
multiline
rows={4}
margin="normal"
error={touched.description && Boolean(errors.description)}
helperText={touched.description && errors.description}
/>
<Field
as={TextField}
name="price"
label="Price (USD)"
type="number"
fullWidth
margin="normal"
error={touched.price && Boolean(errors.price)}
helperText={touched.price && errors.price}
/>
<Field
as={TextField}
name="stock"
label="Stock Quantity"
type="number"
fullWidth
margin="normal"
error={touched.stock && Boolean(errors.stock)}
helperText={touched.stock && errors.stock}
/>
<Field
as={TextField}
name="image"
label="Image URL"
fullWidth
margin="normal"
error={touched.image && Boolean(errors.image)}
helperText={touched.image && errors.image}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 2 }}
>
{isSubmitting ? 'Adding...' : 'Add Product'}
</Button>
</Form>
)}
</Formik>
</Box>
);
};
export default ProductUploader;
7. Implementing Quick Store Pages:
o src/pages/CreateStore.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/pages/CreateStore.js
import React from 'react';
import StoreForm from '../components/Store/StoreForm';
const CreateStore = () => {
return <StoreForm />;
};
export default CreateStore;
o src/pages/Stores.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/pages/Stores.js
import React from 'react';
import StoreList from '../components/Store/StoreList';
const Stores = () => {
return <StoreList />;
};
export default Stores;
o src/pages/StoreDetailsPage.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/pages/StoreDetailsPage.js
import React from 'react';
import StoreDetails from '../components/Store/StoreDetails';
import ProductUploader from '../components/Product/ProductUploader';
import { useParams } from 'react-router-dom';
const StoreDetailsPage = () => {
const { id } = useParams();
return (
<>
<StoreDetails />
<ProductUploader storeId={id} />
{/* Additional store functionalities can be added here */}
</>
);
};
export default StoreDetailsPage;
8. Updating Routing in App.js:
o src/App.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import CreateStore from './pages/CreateStore';
import Stores from './pages/Stores';
import StoreDetailsPage from './pages/StoreDetailsPage';
import Navbar from './components/Navbar'; // Assuming a Navbar component exists
const App = () => {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Stores />} />
<Route path="/create-store" element={<CreateStore />} />
<Route path="/stores/:id" element={<StoreDetailsPage />} />
</Routes>
<ToastContainer />
</Router>
);
};
export default App;
9. Implementing the Navbar (Optional):
o src/components/Navbar.js:
javascript
Copy code
// frontend/quick-store-dashboard/src/components/Navbar.js
import React from 'react';
import { AppBar, Toolbar, Typography, Button } from '@mui/material';
import { Link } from 'react-router-dom';
const Navbar = () => {
const handleLogout = () => {
localStorage.removeItem('token');
window.location.reload();
};
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Quick Store Platform
</Typography>
<Button color="inherit" component={Link} to="/">
Stores
</Button>
<Button color="inherit" component={Link} to="/create-store">
Create Store
</Button>
<Button color="inherit" onClick={handleLogout}>
Logout
</Button>
</Toolbar>
</AppBar>
);
};
export default Navbar;
10. Testing the Quick Store Service:
o Run Backend Tests:
bash
Copy code
cd store-service
npm test
o Run Frontend Tests:
bash
Copy code
cd frontend/quick-store-dashboard
npm test
(Implement additional tests for frontend components as needed.)
11. Updating Docker Compose Configuration:
o Add the Store Service and Frontend to docker-compose.yml:
yaml
Copy code
store-service:
build: ./store-service
restart: always
ports:
- "5013:5013"
environment:
- PORT=5013
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/storedb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
frontend-quick-store:
build: ./frontend/quick-store-dashboard
restart: always
expose:
- "3003"
environment:
- REACT_APP_API_BASE_URL=http://store-service:5013/api
depends_on:
- store-service
o Update Nginx Configuration:
Create Server Block for Quick Store Subdomain (quickstore.ibrandidit.com):
/etc/nginx/sites-available/quickstore.ibrandidit.com.conf:
nginx
Copy code
server {
listen 80;
server_name quickstore.ibrandidit.com;
location / {
proxy_pass http://frontend-quick-store:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name quickstore.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/quickstore.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/quickstore.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://frontend-quick-store:3003;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
Enable the Server Block:
bash
Copy code
sudo ln -s /etc/nginx/sites-available/quickstore.ibrandidit.com.conf /etc/nginx/sites-enabled/
Obtain SSL Certificate for Quick Store Subdomain:
bash
Copy code
sudo certbot --nginx -d quickstore.ibrandidit.com
Reload Nginx:
bash
Copy code
sudo systemctl reload nginx
12. Finalizing Docker Compose for Quick Store Frontend:
o Ensure the Quick Store Frontend Dockerfile is correctly set up.
o frontend/quick-store-dashboard/Dockerfile:
dockerfile
Copy code
# frontend/quick-store-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve
RUN npm install -g serve
# Expose port
EXPOSE 3003
# Start the app
CMD ["serve", "-s", "build", "-l", "3003"]
o Rebuild and Restart Docker Containers:
bash
Copy code
docker-compose up --build -d
Ensure all services are up and running:
bash
Copy code
docker-compose ps
3.4. Integrating Fundraising & Quick Store Setup with Existing Services
1. Authentication:
o Ensure that both Fundraising and Quick Store Setup services validate JWT tokens consistently.
o Share the same JWT_SECRET across all services to maintain authentication integrity.
2. PayPal Integration:
o Utilize the same PayPal REST APIs across the platform.
o Centralize PayPal configuration to avoid redundancy.
o Ensure that environment variables for PayPal (PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET) are securely managed.
3. Database Management:
o Maintain separate databases or collections for different services to ensure data isolation and scalability.
o Use MongoDB's referencing (ref) to link related data across services.
4. API Communication:
o Ensure that frontend applications communicate with their respective backend services via the correct API endpoints.
o Utilize Docker Compose service names for inter-service communication within the Docker network.
5. Reverse Proxy Configuration:
o Update Nginx server blocks to route traffic to the correct frontend services based on the requested domain or subdomain.
o Maintain consistency in naming conventions to simplify configurations.
6. Error Handling and Logging:
o Implement centralized error handling middleware in backend services.
o Use Winston for consistent logging across all services.
7. Testing and Quality Assurance:
o Implement unit and integration tests for both backend and frontend components.
o Use Jest and Supertest for backend testing.
o Use React Testing Library and Jest for frontend testing.
________________________________________
4. Documentation and Usage
4.1. Running the Fundraising & Quick Store Setup Services Locally
1. Start Backend Microservices:
Ensure that all backend services, including the newly added Fundraising and Store services, are running.
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
2. Start Frontend Applications:
o Fundraising Frontend:
bash
Copy code
cd frontend/fundraising-dashboard
npm start
Access at http://localhost:3003/ (if running locally without Docker).
o Quick Store Frontend:
bash
Copy code
cd frontend/quick-store-dashboard
npm start
Access at http://localhost:3004/ (if running locally without Docker).
3. Access via Domains/Subdomains:
o Fundraising Platform: https://fundraising.ibrandidit.com
o Quick Store Setup: https://quickstore.ibrandidit.com
4.2. Interacting with Fundraising Features
1. Creating a Campaign
• Navigate to: https://fundraising.ibrandidit.com/create-campaign
• Fill Out the Form:
o Title: Name of the campaign.
o Description: Detailed description of the campaign's purpose.
o Goal Amount: Target fundraising amount in USD.
o Image URL: (Optional) Link to an image representing the campaign.
• Submit: Click on Create Campaign.
• Result: Campaign is created, and a success message is displayed.
2. Viewing Campaigns
• Navigate to: https://fundraising.ibrandidit.com/
• View List: All active campaigns are displayed with details like title, description, goal amount, and current raised amount.
• View Details: Click on View Details to see more information about a specific campaign.
3. Donating to a Campaign
• Navigate to a Campaign Details Page: e.g., https://fundraising.ibrandidit.com/campaigns/:id
• Donate: Click on Donate $50 (or any predefined amount).
• Complete Payment: You are redirected to PayPal to complete the donation.
• Confirmation: Upon successful payment, the campaign's raised amount is updated.
4.3. Interacting with Quick Store Setup Features
1. Creating a Store
• Navigate to: https://quickstore.ibrandidit.com/create-store
• Fill Out the Form:
o Store Name: Name of your online store.
o Description: Detailed description of your store.
o Logo URL: (Optional) Link to your store's logo.
o Theme: Choose between available themes (e.g., Light, Dark).
• Submit: Click on Create Store.
• Result: Store is created, and a success message is displayed.
2. Viewing Stores
• Navigate to: https://quickstore.ibrandidit.com/
• View List: All available stores are displayed with details like name, description, theme, and logo.
• View Details: Click on View Store to see more information about a specific store.
3. Adding Products to a Store
• Navigate to a Store Details Page: e.g., https://quickstore.ibrandidit.com/stores/:id
• Add Product: Fill out the product form with details like name, description, price, stock quantity, and image URL.
• Submit: Click on Add Product.
• Result: Product is added to the store, and a success message is displayed.
4.4. API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• Fundraising Service: https://fundraising.ibrandidit.com/api-docs
• Store Service: https://quickstore.ibrandidit.com/api-docs
• Other Services:
o Auth Service: https://ibrandidit.com/api-docs
o Order Service: https://ibrandidit.com/api-docs
o Payment Service: https://ibrandidit.com/api-docs
o Shipping Service: https://ibrandidit.com/api-docs
o AI Service: https://ibrandidit.com/api-docs
o Product Customization Service: https://ibrandidit.com/api-docs
o Affiliate & Distributor Management Service: https://ibrandidit.com/api-docs
o Inventory Management Service: https://ibrandidit.com/api-docs
4.5. Testing
Backend Testing
1. Run All Backend Service Tests:
bash
Copy code
# For each backend service, run:
cd path/to/service
npm test
o Ensure all tests pass, verifying the functionality of each API endpoint.
Frontend Testing
1. Run All Frontend Tests:
bash
Copy code
# For each frontend application, run:
cd path/to/frontend
npm test
o Ensure that all tests pass, especially those related to fundraising and store setup functionalities.
4.6. Linting and Formatting
Maintain code quality and consistency using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd path/to/service-or-frontend
npm run lint
2. Run Formatting:
bash
Copy code
cd path/to/service-or-frontend
npm run format
4.7. Deployment
1. Ensure All Services Are Containerized:
o All backend and frontend services should have Dockerfiles.
2. Build and Deploy with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
3. Verify Deployment:
o Access each domain and subdomain to ensure they're operational and serving the correct frontend applications.
4. Monitor Services:
o Use Docker logs to monitor service health.
bash
Copy code
docker-compose logs -f
4.8. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/fundraising-quick-store
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: add fundraising and quick store setup features"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/fundraising-quick-store
o Submit a pull request from your fork to the main repository for review.
________________________________________
5. Complete Code and Configuration Files
5.1. Fundraising Service
• Directory Structure:
bash
Copy code
fundraising-service/
├── controllers/
│ └── campaignController.js
├── models/
│ └── Campaign.js
├── routes/
│ └── campaignRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ └── campaignController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
• Key Files:
o models/Campaign.js
(As provided in Campaign Model)
o controllers/campaignController.js
(As provided in Campaign Controller)
o routes/campaignRoutes.js
(As provided in Campaign Routes)
o middleware/authenticate.js
(As provided in Authentication Middleware)
o middleware/errorHandler.js
(As provided in Error Handling Middleware)
o utils/logger.js
(As provided in Logger Utility)
o index.js
(As provided in Main Application File)
o tests/campaignController.test.js
(As provided in Testing the Campaign Controller)
5.2. Quick Store Service
• Directory Structure:
bash
Copy code
store-service/
├── controllers/
│ └── storeController.js
├── models/
│ └── Store.js
├── routes/
│ └── storeRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ └── storeController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
• Key Files:
o models/Store.js:
javascript
Copy code
// store-service/models/Store.js
const mongoose = require('mongoose');
const StoreSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
logo: {
type: String,
},
theme: {
type: String,
enum: ['light', 'dark'],
default: 'light',
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
products: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
},
],
},
{ timestamps: true }
);
module.exports = mongoose.model('Store', StoreSchema);
o controllers/storeController.js:
javascript
Copy code
// store-service/controllers/storeController.js
const Store = require('../models/Store');
const Product = require('../models/Product');
// Create a new store
const createStore = async (req, res) => {
const { name, description, logo, theme } = req.body;
const creatorId = req.user.id;
try {
const store = new Store({
name,
description,
logo,
theme,
creator: creatorId,
});
await store.save();
res.status(201).json({ message: 'Store created successfully', store });
} catch (error) {
console.error('Error creating store:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get all stores
const getAllStores = async (req, res) => {
try {
const stores = await Store.find().populate('creator', 'name email').populate('products');
res.status(200).json({ stores });
} catch (error) {
console.error('Error fetching stores:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get store by ID
const getStoreById = async (req, res) => {
const { id } = req.params;
try {
const store = await Store.findById(id).populate('creator', 'name email').populate('products');
if (!store) {
return res.status(404).json({ message: 'Store not found' });
}
res.status(200).json({ store });
} catch (error) {
console.error('Error fetching store:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Add a product to a store
const addProductToStore = async (req, res) => {
const { id } = req.params; // Store ID
const { name, description, price, stock, image } = req.body;
try {
const store = await Store.findById(id);
if (!store) {
return res.status(404).json({ message: 'Store not found' });
}
// Create a new product
const product = new Product({
name,
description,
price,
stock,
image,
category: 'Store Product', // Example category
});
await product.save();
// Add product to store's products array
store.products.push(product._id);
await store.save();
res.status(201).json({ message: 'Product added to store successfully', product });
} catch (error) {
console.error('Error adding product to store:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
createStore,
getAllStores,
getStoreById,
addProductToStore,
};
o routes/storeRoutes.js:
javascript
Copy code
// store-service/routes/storeRoutes.js
const express = require('express');
const {
createStore,
getAllStores,
getStoreById,
addProductToStore,
} = require('../controllers/storeController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Public Routes
router.get('/', getAllStores);
router.get('/:id', getStoreById);
// Protected Routes
router.post('/', authenticate, createStore);
router.post('/:id/products', authenticate, addProductToStore);
module.exports = router;
o middleware/authenticate.js:
(Same as previous services)
javascript
Copy code
// store-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
o middleware/errorHandler.js:
(Same as previous services)
javascript
Copy code
// store-service/middleware/errorHandler.js
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
};
module.exports = errorHandler;
o utils/logger.js:
(Same as previous services)
javascript
Copy code
// store-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'store-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
o index.js:
javascript
Copy code
// store-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const storeRoutes = require('./routes/storeRoutes');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/stores', storeRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Store Service API');
});
// Error Handling Middleware
app.use(errorHandler);
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5013;
app.listen(PORT, () => {
logger.info(`Store Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
o tests/storeController.test.js:
javascript
Copy code
// store-service/tests/storeController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const Store = require('../models/Store');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Store API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await Store.deleteMany();
});
describe('POST /api/stores', () => {
it('should create a new store', async () => {
const res = await request(app)
.post('/api/stores')
.set('Authorization', `Bearer ${token}`)
.send({
name: 'My Online Store',
description: 'A store to sell awesome products.',
logo: 'http://example.com/logo.png',
theme: 'light',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Store created successfully');
expect(res.body.store).toHaveProperty('name', 'My Online Store');
});
it('should not allow unauthenticated store creation', async () => {
const res = await request(app).post('/api/stores').send({
name: 'Unauthorized Store',
description: 'Should not be created.',
theme: 'dark',
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
describe('GET /api/stores', () => {
it('should fetch all stores', async () => {
await Store.create([
{
name: 'Store 1',
description: 'Description 1',
creator: userPayload.id,
},
{
name: 'Store 2',
description: 'Description 2',
creator: userPayload.id,
},
]);
const res = await request(app).get('/api/stores').send();
expect(res.statusCode).toEqual(200);
expect(res.body.stores).toHaveLength(2);
});
});
describe('GET /api/stores/:id', () => {
it('should fetch a single store by ID', async () => {
const store = await Store.create({
name: 'Store 3',
description: 'Description 3',
creator: userPayload.id,
});
const res = await request(app).get(`/api/stores/${store._id}`).send();
expect(res.statusCode).toEqual(200);
expect(res.body.store).toHaveProperty('name', 'Store 3');
});
it('should return 404 if store not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app).get(`/api/stores/${nonExistentId}`).send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Store not found');
});
});
describe('POST /api/stores/:id/products', () => {
it('should add a product to the store', async () => {
const store = await Store.create({
name: 'Store 4',
description: 'Description 4',
creator: userPayload.id,
});
const res = await request(app)
.post(`/api/stores/${store._id}/products`)
.set('Authorization', `Bearer ${token}`)
.send({
name: 'Product 1',
description: 'Product Description',
price: 25.5,
stock: 100,
image: 'http://example.com/product1.png',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Product added to store successfully');
expect(res.body.product).toHaveProperty('name', 'Product 1');
});
it('should not allow unauthenticated product addition', async () => {
const store = await Store.create({
name: 'Store 5',
description: 'Description 5',
creator: userPayload.id,
});
const res = await request(app).post(`/api/stores/${store._id}/products`).send({
name: 'Product 2',
description: 'Another Product',
price: 30.0,
stock: 50,
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
});
14. Store Service Main Application File:
o index.js:
javascript
Copy code
// store-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const storeRoutes = require('./routes/storeRoutes');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/stores', storeRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('Store Service API');
});
// Error Handling Middleware
app.use(errorHandler);
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5013;
app.listen(PORT, () => {
logger.info(`Store Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
15. Store Service Logger Utility:
o utils/logger.js:
(As provided in Logger Utility)
javascript
Copy code
// store-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'store-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
16. Quick Store Frontend Components:
(As provided in Quick Store Frontend Components)
5.2. Nginx Configuration Files
(Assuming previous steps included creating server blocks for fundraising and quick store subdomains.)
5.3. Docker Compose File
• docker-compose.yml:
yaml
Copy code
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
# ... Other backend services
fundraising-service:
build: ./fundraising-service
restart: always
ports:
- "5012:5012"
environment:
- PORT=5012
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/fundraisingdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- PAYPAL_MODE=sandbox
- PAYPAL_CLIENT_ID=your_paypal_client_id
- PAYPAL_CLIENT_SECRET=your_paypal_client_secret
depends_on:
- mongodb
- rabbitmq
store-service:
build: ./store-service
restart: always
ports:
- "5013:5013"
environment:
- PORT=5013
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/storedb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672
depends_on:
- mongodb
- rabbitmq
# ... Other backend services
frontend:
build: ./frontend/user-dashboard
restart: always
expose:
- "3000"
environment:
- REACT_APP_API_BASE_URL=http://order-service:5004
- REACT_APP_SHIPPING_SERVICE_URL=http://shipping-service:5006
- REACT_APP_PAYMENT_SERVICE_URL=http://payment-service:5005
- REACT_APP_AUTH_SERVICE_URL=http://auth-service:5003
- REACT_APP_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
- REACT_APP_AI_SERVICE_URL=http://ai-service:5007/api/ai
- REACT_APP_CUSTOMIZATION_SERVICE_URL=http://product-customization-service:5008/api/customizations
- REACT_APP_AFFILIATE_SERVICE_URL=http://affiliate-distributor-service:5009/api/affiliates
- REACT_APP_DISTRIBUTOR_SERVICE_URL=http://affiliate-distributor-service:5009/api/distributors
depends_on:
- auth-service
- order-service
- payment-service
- shipping-service
- ai-service
- product-customization-service
- affiliate-distributor-service
fundraising-frontend:
build: ./frontend/fundraising-dashboard
restart: always
expose:
- "3003"
environment:
- REACT_APP_API_BASE_URL=http://fundraising-service:5012/api
depends_on:
- fundraising-service
quick-store-frontend:
build: ./frontend/quick-store-dashboard
restart: always
expose:
- "3004"
environment:
- REACT_APP_API_BASE_URL=http://store-service:5013/api
depends_on:
- store-service
# Nginx reverse proxy
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/sites-available:/etc/nginx/sites-available
- ./nginx/sites-enabled:/etc/nginx/sites-enabled
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- shop-frontend
- blog-frontend
- fundraising-frontend
- quick-store-frontend
# ... Other services
volumes:
mongo-data:
5.4. Frontend Applications
(As detailed in Frontend Implementation)
5.5. Backend Services
(As detailed in Backend Implementation)
________________________________________
6. Troubleshooting Tips
1. Nginx Not Routing Correctly:
o Problem: Requests to fundraising or quick store subdomains are not reaching the intended frontend service.
o Solution:
Verify that the server blocks in Nginx are correctly configured.
Ensure that Docker Compose service names (fundraising-frontend, quick-store-frontend) match the proxy pass URLs in Nginx configuration.
Check Nginx logs for any errors:
bash
Copy code
sudo tail -f /var/log/nginx/error.log
2. SSL Certificate Issues:
o Problem: Browsers show certificate warnings or errors.
o Solution:
Ensure that SSL certificates are correctly installed and renewed.
Verify that the certificate paths in the Nginx configuration are accurate.
Use sudo certbot renew to manually renew certificates if automatic renewal fails.
3. DNS Propagation Delays:
o Problem: Newly created subdomains are not resolving.
o Solution:
Wait for DNS changes to propagate (up to 48 hours).
Use tools like WhatsMyDNS to check DNS propagation status.
4. Frontend Services Not Responding:
o Problem: Accessing a subdomain returns a blank page or 502 Bad Gateway.
o Solution:
Ensure that the frontend services are running and healthy.
Check Docker logs for the respective frontend service:
bash
Copy code
docker-compose logs fundraising-frontend
docker-compose logs quick-store-frontend
Verify that the proxy pass URLs in Nginx are correct.
5. Authentication Errors:
o Problem: Users receive 401 Unauthorized errors when accessing APIs from fundraising or quick store services.
o Solution:
Ensure that JWT tokens are correctly passed in the Authorization header.
Verify that the authentication middleware is correctly implemented in backend services.
Check that all services share the same JWT_SECRET.
6. Database Connection Issues:
o Problem: Services cannot connect to MongoDB.
o Solution:
Ensure that MongoDB is running and accessible.
Verify the MONGODB_URI in the .env files.
Check network configurations and firewall settings.
7. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
8. Resource Conflicts:
o Problem: Multiple services are trying to use the same port.
o Solution:
Ensure that each service in Docker Compose has unique port mappings.
Use internal port exposure (expose) and let Nginx handle external port assignments.
9. Form Validation Errors:
o Problem: Users cannot submit forms on fundraising or quick store dashboards due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
10. Performance Bottlenecks:
o Problem: Fundraising or quick store features are loading slowly.
o Solution:
Enable caching in Nginx.
Optimize frontend assets (minification, image optimization).
Scale services horizontally if necessary.
________________________________________
7. Additional Tools and Best Practices
1. Rate Limiting:
o Prevent abuse by limiting the number of requests per user/IP.
o Already implemented using express-rate-limit in backend services.
2. Caching:
o Implement caching for frequently accessed data to reduce database load and improve response times.
o Use Nginx caching or in-memory caching solutions like Redis if necessary.
3. Logging:
o Implement comprehensive logging to monitor service performance and debug issues.
o Use Winston for structured logging in backend services.
4. Security:
o Input Validation: Use libraries like express-validator to validate and sanitize user inputs.
o Helmet: Already implemented to set secure HTTP headers.
o HTTPS: Ensure all communications are over HTTPS in production environments.
5. Error Handling:
o Implement centralized error handling to provide meaningful feedback to users and maintain application stability.
o Use error-handling middleware in backend services.
6. Responsive Design:
o Ensure that fundraising and quick store setup dashboards are responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
7. Version Control:
o Commit changes regularly with clear messages.
o Use feature branches for new functionalities.
o Merge changes via pull requests with code reviews.
8. Documentation Maintenance:
o Ensure that all changes are reflected in the documentation.
o Encourage team members to contribute to documentation improvements.
________________________________________
8. Complete Code and Configuration Files
8.1. Fundraising Service
(As detailed in Backend Implementation)
8.2. Quick Store Service
(As detailed in Quick Store Service)
8.3. Frontend Applications
(As detailed in Frontend Implementation)
8.4. Docker Compose File
(As detailed in Docker Compose Configuration)
8.5. Nginx Configuration Files
(As detailed in Nginx Configuration Files)
________________________________________
9. Conclusion
By meticulously following this guide for Stage 19: Fundraising & Quick Store Setup, you have successfully integrated robust fundraising capabilities and streamlined store setup processes into your eCommerce platform. These enhancements not only empower users to engage in fundraising activities but also simplify the creation and management of their online storefronts, fostering a more dynamic and user-friendly ecosystem.
Next Steps:
Proceed to Stage 20: Analytics & Reporting Integration to incorporate comprehensive analytics and reporting tools, enabling data-driven decision-making and performance tracking across your platform.
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 19: Fundraising & Quick Store Setup within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced security measures, integrating with third-party services for enhanced functionalities, or customizing features to cater to specific user needs.
________________________________________
Stage 20: BaaS Integration for Debit Card and FinTech Partnership is the final and crucial phase in enhancing your eCommerce platform. This stage focuses on integrating Backend-as-a-Service (BaaS) features to facilitate debit card transactions and establish partnerships with FinTech providers. By incorporating these functionalities, your platform will offer seamless financial services, enhancing user trust and expanding monetization opportunities.
This comprehensive guide is tailored for beginners and includes complete code snippets, dependencies, configuration files, and step-by-step documentation to ensure easy integration. Each stage builds upon the previous ones, maintaining consistency and handling dependencies effectively. The final product will be a fully deployable solution where minimal configuration adjustments are required, allowing developers to focus on uploading images, content, and fine-tuning settings.
________________________________________
Stage 20: BaaS Integration for Debit Card and FinTech Partnership
________________________________________
1. Overview
Objective:
• Debit Card Integration:
o Enable users to add, manage, and use debit cards for transactions.
o Facilitate seamless debit card payments within the platform.
• FinTech Partnership:
o Integrate with FinTech providers to offer enhanced financial services.
o Implement features like instant payouts, financial analytics, and account management.
Benefits:
• Enhanced Payment Options: Providing debit card payments increases transaction convenience for users.
• Financial Services Expansion: Partnerships with FinTech companies enable a broader range of financial functionalities.
• Increased Trust and Security: Utilizing established FinTech services ensures secure and reliable financial transactions.
• Monetization Opportunities: Offering financial services can open new revenue streams and improve user retention.
________________________________________
2. Technology Stack
• Backend Framework: Node.js with Express.js
• Frontend Framework: React.js
• Database: MongoDB – Using Mongoose ODM
• Authentication: JWT (JSON Web Tokens) – Existing setup
• Payment Integration: Stripe API (recommended for debit card processing)
• FinTech Integration: Plaid API (for connecting user bank accounts)
• Containerization: Docker
• Orchestration: Docker Compose
• API Documentation: Swagger
• Testing Frameworks: Jest and Supertest
• Logging: Winston
• Error Handling: Centralized middleware
• Environment Variables Management: dotenv
• Version Control: Git
• UI Components: Material-UI
________________________________________
3. Implementation Steps
3.1. Backend Implementation
3.1.1. Create FinTech Service
1. Directory Structure:
bash
Copy code
fintech-service/
├── controllers/
│ ├── cardController.js
│ └── fintechController.js
├── models/
│ ├── DebitCard.js
│ └── FinTechPartner.js
├── routes/
│ ├── cardRoutes.js
│ └── fintechRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ ├── cardController.test.js
│ └── fintechController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
2. Initialize the Project:
bash
Copy code
mkdir fintech-service
cd fintech-service
npm init -y
3. Install Dependencies:
bash
Copy code
npm install express mongoose dotenv cors helmet express-rate-limit winston stripe plaid
npm install --save-dev nodemon eslint prettier jest supertest
4. Configuration Files:
o .env File:
env
Copy code
PORT=5014
NODE_ENV=development
MONGODB_URI=mongodb://root:example@mongodb:27017/fintechdb?authSource=admin
JWT_SECRET=your_jwt_secret_key
STRIPE_SECRET_KEY=your_stripe_secret_key
PLAID_CLIENT_ID=your_plaid_client_id
PLAID_SECRET=your_plaid_secret
PLAID_ENV=sandbox
o package.json Scripts:
json
Copy code
{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"lint": "eslint .",
"format": "prettier --write .",
"test": "jest"
}
}
5. Implementing the DebitCard Model:
o models/DebitCard.js:
javascript
Copy code
// fintech-service/models/DebitCard.js
const mongoose = require('mongoose');
const DebitCardSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
stripeCardId: {
type: String,
required: true,
},
brand: {
type: String,
required: true,
},
last4: {
type: String,
required: true,
},
expMonth: {
type: Number,
required: true,
},
expYear: {
type: Number,
required: true,
},
},
{ timestamps: true }
);
module.exports = mongoose.model('DebitCard', DebitCardSchema);
6. Implementing the FinTechPartner Model:
o models/FinTechPartner.js:
javascript
Copy code
// fintech-service/models/FinTechPartner.js
const mongoose = require('mongoose');
const FinTechPartnerSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
apiKey: {
type: String,
required: true,
},
apiSecret: {
type: String,
required: true,
},
endpoint: {
type: String,
required: true,
},
status: {
type: String,
enum: ['active', 'inactive'],
default: 'active',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('FinTechPartner', FinTechPartnerSchema);
7. Implementing the Card Controller:
o controllers/cardController.js:
javascript
Copy code
// fintech-service/controllers/cardController.js
const DebitCard = require('../models/DebitCard');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const logger = require('../utils/logger');
// Add a new debit card
const addDebitCard = async (req, res) => {
const { paymentMethodId } = req.body;
const userId = req.user.id;
try {
// Attach the payment method to the user in Stripe
const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
customer: req.user.stripeCustomerId, // Assuming user model has stripeCustomerId
});
// Update the user's default payment method
await stripe.customers.update(req.user.stripeCustomerId, {
invoice_settings: {
default_payment_method: paymentMethodId,
},
});
// Create a record in the database
const card = new DebitCard({
user: userId,
stripeCardId: paymentMethod.card.id,
brand: paymentMethod.card.brand,
last4: paymentMethod.card.last4,
expMonth: paymentMethod.card.exp_month,
expYear: paymentMethod.card.exp_year,
});
await card.save();
res.status(201).json({ message: 'Debit card added successfully', card });
} catch (error) {
logger.error('Error adding debit card:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get all debit cards for a user
const getDebitCards = async (req, res) => {
const userId = req.user.id;
try {
const cards = await DebitCard.find({ user: userId });
res.status(200).json({ cards });
} catch (error) {
logger.error('Error fetching debit cards:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Remove a debit card
const removeDebitCard = async (req, res) => {
const { cardId } = req.params;
const userId = req.user.id;
try {
const card = await DebitCard.findOne({ _id: cardId, user: userId });
if (!card) {
return res.status(404).json({ message: 'Debit card not found' });
}
// Detach the payment method from Stripe
await stripe.paymentMethods.detach(card.stripeCardId);
// Remove the card from the database
await DebitCard.deleteOne({ _id: cardId });
res.status(200).json({ message: 'Debit card removed successfully' });
} catch (error) {
logger.error('Error removing debit card:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
addDebitCard,
getDebitCards,
removeDebitCard,
};
8. Implementing the FinTech Controller:
o controllers/fintechController.js:
javascript
Copy code
// fintech-service/controllers/fintechController.js
const FinTechPartner = require('../models/FinTechPartner');
const axios = require('axios');
const logger = require('../utils/logger');
// Add a new FinTech partner
const addFinTechPartner = async (req, res) => {
const { name, apiKey, apiSecret, endpoint } = req.body;
try {
const partner = new FinTechPartner({
name,
apiKey,
apiSecret,
endpoint,
});
await partner.save();
res.status(201).json({ message: 'FinTech partner added successfully', partner });
} catch (error) {
logger.error('Error adding FinTech partner:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Get all FinTech partners
const getFinTechPartners = async (req, res) => {
try {
const partners = await FinTechPartner.find();
res.status(200).json({ partners });
} catch (error) {
logger.error('Error fetching FinTech partners:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Update a FinTech partner
const updateFinTechPartner = async (req, res) => {
const { partnerId } = req.params;
const { name, apiKey, apiSecret, endpoint, status } = req.body;
try {
const partner = await FinTechPartner.findById(partnerId);
if (!partner) {
return res.status(404).json({ message: 'FinTech partner not found' });
}
partner.name = name || partner.name;
partner.apiKey = apiKey || partner.apiKey;
partner.apiSecret = apiSecret || partner.apiSecret;
partner.endpoint = endpoint || partner.endpoint;
partner.status = status || partner.status;
await partner.save();
res.status(200).json({ message: 'FinTech partner updated successfully', partner });
} catch (error) {
logger.error('Error updating FinTech partner:', error);
res.status(500).json({ message: 'Server Error' });
}
};
// Remove a FinTech partner
const removeFinTechPartner = async (req, res) => {
const { partnerId } = req.params;
try {
const partner = await FinTechPartner.findById(partnerId);
if (!partner) {
return res.status(404).json({ message: 'FinTech partner not found' });
}
await FinTechPartner.deleteOne({ _id: partnerId });
res.status(200).json({ message: 'FinTech partner removed successfully' });
} catch (error) {
logger.error('Error removing FinTech partner:', error);
res.status(500).json({ message: 'Server Error' });
}
};
module.exports = {
addFinTechPartner,
getFinTechPartners,
updateFinTechPartner,
removeFinTechPartner,
};
9. Implementing the Card Routes:
o routes/cardRoutes.js:
javascript
Copy code
// fintech-service/routes/cardRoutes.js
const express = require('express');
const {
addDebitCard,
getDebitCards,
removeDebitCard,
} = require('../controllers/cardController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Protected Routes
router.post('/', authenticate, addDebitCard);
router.get('/', authenticate, getDebitCards);
router.delete('/:cardId', authenticate, removeDebitCard);
module.exports = router;
10. Implementing the FinTech Routes:
o routes/fintechRoutes.js:
javascript
Copy code
// fintech-service/routes/fintechRoutes.js
const express = require('express');
const {
addFinTechPartner,
getFinTechPartners,
updateFinTechPartner,
removeFinTechPartner,
} = require('../controllers/fintechController');
const authenticate = require('../middleware/authenticate');
const router = express.Router();
// Protected Routes (Assuming only admins can manage partners)
router.post('/', authenticate, addFinTechPartner);
router.get('/', authenticate, getFinTechPartners);
router.put('/:partnerId', authenticate, updateFinTechPartner);
router.delete('/:partnerId', authenticate, removeFinTechPartner);
module.exports = router;
11. Authentication Middleware:
o middleware/authenticate.js:
(Assuming similar to previous services)
javascript
Copy code
// fintech-service/middleware/authenticate.js
const jwt = require('jsonwebtoken');
const dotenv = require('dotenv');
dotenv.config();
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized: No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Assuming payload contains user info
next();
} catch (error) {
return res.status(401).json({ message: 'Unauthorized: Invalid token' });
}
};
module.exports = authenticate;
12. Error Handling Middleware:
o middleware/errorHandler.js:
javascript
Copy code
// fintech-service/middleware/errorHandler.js
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
logger.error(`${req.method} ${req.url} - ${err.message}`);
res.status(500).json({ message: 'Internal Server Error' });
};
module.exports = errorHandler;
13. Logger Utility:
o utils/logger.js:
javascript
Copy code
// fintech-service/utils/logger.js
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'fintech-service' },
transports: [
new transports.Console(),
// Add file transports if needed
// new transports.File({ filename: 'error.log', level: 'error' }),
// new transports.File({ filename: 'combined.log' }),
],
});
module.exports = logger;
14. Main Application File:
o index.js:
javascript
Copy code
// fintech-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cardRoutes = require('./routes/cardRoutes');
const fintechRoutes = require('./routes/fintechRoutes');
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/cards', cardRoutes);
app.use('/api/fintech-partners', fintechRoutes);
// Root Route
app.get('/', (req, res) => {
res.send('FinTech Service API');
});
// Error Handling Middleware
app.use(errorHandler);
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5014;
app.listen(PORT, () => {
logger.info(`FinTech Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
15. Implementing Stripe Webhooks (Optional for Advanced Integration):
o controllers/webhookController.js:
javascript
Copy code
// fintech-service/controllers/webhookController.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const logger = require('../utils/logger');
// Handle Stripe webhooks
const handleStripeWebhook = async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
logger.error('Webhook signature verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'payment_method.attached':
const paymentMethod = event.data.object;
// Then define and call a function to handle the event payment_method.attached
break;
// ... handle other event types
default:
logger.warn(`Unhandled event type ${event.type}`);
}
// Return a 200 response to acknowledge receipt of the event
res.json({ received: true });
};
module.exports = {
handleStripeWebhook,
};
o Update Routes:
javascript
Copy code
// fintech-service/routes/webhookRoutes.js
const express = require('express');
const { handleStripeWebhook } = require('../controllers/webhookController');
const bodyParser = require('body-parser');
const router = express.Router();
// Stripe requires the raw body to construct the event
router.post('/stripe', bodyParser.raw({ type: 'application/json' }), handleStripeWebhook);
module.exports = router;
o Update index.js to Use Webhook Routes:
javascript
Copy code
// fintech-service/index.js
// ... existing imports
const webhookRoutes = require('./routes/webhookRoutes');
// ... existing middleware
// Webhook Route (must come before JSON parsing middleware if not using raw body elsewhere)
app.use('/api/webhooks', webhookRoutes);
// ... existing routes and error handling
16. Testing the Card Controller:
o tests/cardController.test.js:
javascript
Copy code
// fintech-service/tests/cardController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const DebitCard = require('../models/DebitCard');
// Mock authentication middleware
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Test User',
email: 'testuser@example.com',
stripeCustomerId: 'cus_testcustomer123', // Mock Stripe customer ID
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('Debit Card API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await DebitCard.deleteMany();
});
describe('POST /api/cards', () => {
it('should add a new debit card', async () => {
const res = await request(app)
.post('/api/cards')
.set('Authorization', `Bearer ${token}`)
.send({
paymentMethodId: 'pm_card_visa', // Mock payment method ID
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'Debit card added successfully');
expect(res.body.card).toHaveProperty('brand', 'Visa');
});
it('should not allow unauthenticated debit card addition', async () => {
const res = await request(app).post('/api/cards').send({
paymentMethodId: 'pm_card_mastercard',
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
describe('GET /api/cards', () => {
it('should fetch all debit cards for a user', async () => {
await DebitCard.create([
{
user: userPayload.id,
stripeCardId: 'card_1',
brand: 'Visa',
last4: '4242',
expMonth: 12,
expYear: 2025,
},
{
user: userPayload.id,
stripeCardId: 'card_2',
brand: 'Mastercard',
last4: '5555',
expMonth: 11,
expYear: 2024,
},
]);
const res = await request(app).get('/api/cards').set('Authorization', `Bearer ${token}`).send();
expect(res.statusCode).toEqual(200);
expect(res.body.cards).toHaveLength(2);
});
});
describe('DELETE /api/cards/:cardId', () => {
it('should remove a debit card', async () => {
const card = await DebitCard.create({
user: userPayload.id,
stripeCardId: 'card_3',
brand: 'American Express',
last4: '3782',
expMonth: 10,
expYear: 2026,
});
const res = await request(app)
.delete(`/api/cards/${card._id}`)
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'Debit card removed successfully');
});
it('should return 404 if debit card not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app)
.delete(`/api/cards/${nonExistentId}`)
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'Debit card not found');
});
});
});
17. Testing the FinTech Controller:
o tests/fintechController.test.js:
javascript
Copy code
// fintech-service/tests/fintechController.test.js
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const app = require('../index');
const FinTechPartner = require('../models/FinTechPartner');
// Mock authentication middleware (assuming admin privileges)
const userPayload = {
id: new mongoose.Types.ObjectId(),
name: 'Admin User',
email: 'admin@example.com',
role: 'admin',
};
const token = jwt.sign(userPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
describe('FinTech Partner API', () => {
beforeAll(async () => {
// Connect to in-memory MongoDB server or use a test database
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
});
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});
afterEach(async () => {
await FinTechPartner.deleteMany();
});
describe('POST /api/fintech-partners', () => {
it('should add a new FinTech partner', async () => {
const res = await request(app)
.post('/api/fintech-partners')
.set('Authorization', `Bearer ${token}`)
.send({
name: 'FinTech Co.',
apiKey: 'fintech_api_key',
apiSecret: 'fintech_api_secret',
endpoint: 'https://api.fintechco.com',
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('message', 'FinTech partner added successfully');
expect(res.body.partner).toHaveProperty('name', 'FinTech Co.');
});
it('should not allow unauthenticated FinTech partner addition', async () => {
const res = await request(app).post('/api/fintech-partners').send({
name: 'Unauthorized FinTech',
apiKey: 'unauth_api_key',
apiSecret: 'unauth_api_secret',
endpoint: 'https://api.unauthfintech.com',
});
expect(res.statusCode).toEqual(401);
expect(res.body).toHaveProperty('message', 'Unauthorized: No token provided');
});
});
describe('GET /api/fintech-partners', () => {
it('should fetch all FinTech partners', async () => {
await FinTechPartner.create([
{
name: 'FinTech A',
apiKey: 'api_key_a',
apiSecret: 'api_secret_a',
endpoint: 'https://api.fintecha.com',
},
{
name: 'FinTech B',
apiKey: 'api_key_b',
apiSecret: 'api_secret_b',
endpoint: 'https://api.fintechb.com',
},
]);
const res = await request(app)
.get('/api/fintech-partners')
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(200);
expect(res.body.partners).toHaveLength(2);
});
});
describe('PUT /api/fintech-partners/:partnerId', () => {
it('should update a FinTech partner', async () => {
const partner = await FinTechPartner.create({
name: 'FinTech C',
apiKey: 'api_key_c',
apiSecret: 'api_secret_c',
endpoint: 'https://api.fintechc.com',
});
const res = await request(app)
.put(`/api/fintech-partners/${partner._id}`)
.set('Authorization', `Bearer ${token}`)
.send({
name: 'FinTech C Updated',
status: 'inactive',
});
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'FinTech partner updated successfully');
expect(res.body.partner).toHaveProperty('name', 'FinTech C Updated');
expect(res.body.partner).toHaveProperty('status', 'inactive');
});
it('should return 404 if FinTech partner not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app)
.put(`/api/fintech-partners/${nonExistentId}`)
.set('Authorization', `Bearer ${token}`)
.send({
name: 'Non-Existent FinTech',
});
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'FinTech partner not found');
});
});
describe('DELETE /api/fintech-partners/:partnerId', () => {
it('should remove a FinTech partner', async () => {
const partner = await FinTechPartner.create({
name: 'FinTech D',
apiKey: 'api_key_d',
apiSecret: 'api_secret_d',
endpoint: 'https://api.fintechd.com',
});
const res = await request(app)
.delete(`/api/fintech-partners/${partner._id}`)
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(200);
expect(res.body).toHaveProperty('message', 'FinTech partner removed successfully');
});
it('should return 404 if FinTech partner not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app)
.delete(`/api/fintech-partners/${nonExistentId}`)
.set('Authorization', `Bearer ${token}`)
.send();
expect(res.statusCode).toEqual(404);
expect(res.body).toHaveProperty('message', 'FinTech partner not found');
});
});
});
18. FinTech Service Main Application File:
o index.js:
javascript
Copy code
// fintech-service/index.js
const express = require('express');
const dotenv = require('dotenv');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cardRoutes = require('./routes/cardRoutes');
const fintechRoutes = require('./routes/fintechRoutes');
const webhookRoutes = require('./routes/webhookRoutes'); // If implementing webhooks
const errorHandler = require('./middleware/errorHandler');
const logger = require('./utils/logger');
dotenv.config();
const app = express();
// Security Middleware
app.use(helmet());
// Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again after 15 minutes',
});
app.use(limiter);
// CORS Middleware
app.use(cors());
// JSON Parsing Middleware
app.use(express.json());
// Routes
app.use('/api/cards', cardRoutes);
app.use('/api/fintech-partners', fintechRoutes);
app.use('/api/webhooks', webhookRoutes); // If implementing webhooks
// Root Route
app.get('/', (req, res) => {
res.send('FinTech Service API');
});
// Error Handling Middleware
app.use(errorHandler);
// Connect to MongoDB and Start Server
mongoose
.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
logger.info('MongoDB Connected');
const PORT = process.env.PORT || 5014;
app.listen(PORT, () => {
logger.info(`FinTech Service running on port ${PORT}`);
});
})
.catch((err) => {
logger.error('MongoDB connection error:', err);
process.exit(1);
});
module.exports = app; // Export app for testing
19. Implementing the FinTech Partner Controller:
(As provided in Implementing the FinTech Controller)
20. Implementing the Store Service (if not already done):
(Assuming Stage 20 builds upon Stage 19, and Stage 19 already included Quick Store Setup.)
3.2. Frontend Implementation
3.2.1. Create FinTech Frontend Components
1. Directory Structure:
bash
Copy code
frontend/fintech-dashboard/
├── src/
│ ├── components/
│ │ ├── DebitCard/
│ │ │ ├── DebitCardForm.js
│ │ │ ├── DebitCardList.js
│ │ │ └── DebitCardItem.js
│ │ ├── FinTechPartner/
│ │ │ ├── FinTechPartnerForm.js
│ │ │ ├── FinTechPartnerList.js
│ │ │ └── FinTechPartnerItem.js
│ │ └── Transaction/
│ │ └── TransactionList.js
│ ├── pages/
│ │ ├── AddDebitCard.js
│ │ ├── ManageFinTechPartners.js
│ │ └── Transactions.js
│ ├── services/
│ │ └── fintechApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
2. Initialize the Project:
bash
Copy code
mkdir -p frontend/fintech-dashboard
cd frontend/fintech-dashboard
npx create-react-app .
3. Install Dependencies:
bash
Copy code
npm install axios react-router-dom formik yup react-toastify @stripe/react-stripe-js @stripe/stripe-js
npm install --save-dev @testing-library/react @testing-library/jest-dom jest
4. Configuration Files:
o .env File:
env
Copy code
REACT_APP_API_BASE_URL=http://fintech-service:5014/api
REACT_APP_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
o package.json Scripts:
(Already set by Create React App)
5. Implementing the FinTech API Service:
o src/services/fintechApi.js:
javascript
Copy code
// frontend/fintech-dashboard/src/services/fintechApi.js
import axios from 'axios';
const fintechApi = axios.create({
baseURL: process.env.REACT_APP_API_BASE_URL,
});
// Add a request interceptor to include JWT token
fintechApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token'); // Assuming token is stored in localStorage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
export default fintechApi;
6. Implementing Debit Card Components:
o src/components/DebitCard/DebitCardForm.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/DebitCard/DebitCardForm.js
import React from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import { Button, Box, Typography } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
const DebitCardSchema = Yup.object().shape({});
const DebitCardFormComponent = () => {
const stripe = useStripe();
const elements = useElements();
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
if (!stripe || !elements) {
return;
}
const cardElement = elements.getElement(CardElement);
try {
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
toast.error(error.message);
setSubmitting(false);
return;
}
// Send paymentMethod.id to the backend to attach the card
const res = await fintechApi.post('/cards', {
paymentMethodId: paymentMethod.id,
});
toast.success('Debit card added successfully');
resetForm();
} catch (err) {
console.error(err);
toast.error(err.response?.data?.message || 'Failed to add debit card');
} finally {
setSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, margin: 'auto', padding: 2 }}>
<Typography variant="h5" gutterBottom>
Add a Debit Card
</Typography>
<Formik
initialValues={{}}
validationSchema={DebitCardSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form>
<CardElement options={{ hidePostalCode: true }} />
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting || !stripe}
sx={{ mt: 2 }}
>
{isSubmitting ? 'Adding...' : 'Add Debit Card'}
</Button>
</Form>
)}
</Formik>
</Box>
);
};
const DebitCardForm = () => (
<Elements stripe={stripePromise}>
<DebitCardFormComponent />
</Elements>
);
export default DebitCardForm;
o src/components/DebitCard/DebitCardList.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/DebitCard/DebitCardList.js
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, Typography, Button } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
const DebitCardList = () => {
const [cards, setCards] = useState([]);
useEffect(() => {
const fetchCards = async () => {
try {
const res = await fintechApi.get('/cards');
setCards(res.data.cards);
} catch (error) {
console.error(error);
toast.error('Failed to fetch debit cards');
}
};
fetchCards();
}, []);
const handleRemove = async (cardId) => {
try {
await fintechApi.delete(`/cards/${cardId}`);
toast.success('Debit card removed successfully');
setCards(cards.filter((card) => card._id !== cardId));
} catch (error) {
console.error(error);
toast.error('Failed to remove debit card');
}
};
return (
<Grid container spacing={2} sx={{ padding: 2 }}>
{cards.map((card) => (
<Grid item xs={12} md={4} key={card._id}>
<Card>
<CardContent>
<Typography variant="h6" component="div">
{card.brand} **** **** **** {card.last4}
</Typography>
<Typography color="text.secondary">
Expires: {card.expMonth}/{card.expYear}
</Typography>
<Button
variant="outlined"
color="secondary"
sx={{ mt: 2 }}
onClick={() => handleRemove(card._id)}
>
Remove Card
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};
export default DebitCardList;
7. Implementing FinTech Partner Components:
o src/components/FinTechPartner/FinTechPartnerForm.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/FinTechPartner/FinTechPartnerForm.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import { TextField, Button, Box, Typography } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
const FinTechPartnerSchema = Yup.object().shape({
name: Yup.string().required('Partner name is required'),
apiKey: Yup.string().required('API Key is required'),
apiSecret: Yup.string().required('API Secret is required'),
endpoint: Yup.string().url('Invalid URL').required('Endpoint is required'),
});
const FinTechPartnerForm = () => {
const handleSubmit = async (values, { setSubmitting, resetForm }) => {
try {
const res = await fintechApi.post('/fintech-partners', values);
toast.success('FinTech partner added successfully');
resetForm();
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to add FinTech partner');
} finally {
setSubmitting(false);
}
};
return (
<Box sx={{ maxWidth: 600, margin: 'auto', padding: 2 }}>
<Typography variant="h5" gutterBottom>
Add a FinTech Partner
</Typography>
<Formik
initialValues={{
name: '',
apiKey: '',
apiSecret: '',
endpoint: '',
}}
validationSchema={FinTechPartnerSchema}
onSubmit={handleSubmit}
>
{({ errors, touched, isSubmitting }) => (
<Form>
<Field
as={TextField}
name="name"
label="Partner Name"
fullWidth
margin="normal"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
/>
<Field
as={TextField}
name="apiKey"
label="API Key"
fullWidth
margin="normal"
error={touched.apiKey && Boolean(errors.apiKey)}
helperText={touched.apiKey && errors.apiKey}
/>
<Field
as={TextField}
name="apiSecret"
label="API Secret"
fullWidth
margin="normal"
error={touched.apiSecret && Boolean(errors.apiSecret)}
helperText={touched.apiSecret && errors.apiSecret}
/>
<Field
as={TextField}
name="endpoint"
label="API Endpoint"
fullWidth
margin="normal"
error={touched.endpoint && Boolean(errors.endpoint)}
helperText={touched.endpoint && errors.endpoint}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 2 }}
>
{isSubmitting ? 'Adding...' : 'Add FinTech Partner'}
</Button>
</Form>
)}
</Formik>
</Box>
);
};
export default FinTechPartnerForm;
o src/components/FinTechPartner/FinTechPartnerList.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/FinTechPartner/FinTechPartnerList.js
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, Typography, Button } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
import FinTechPartnerItem from './FinTechPartnerItem';
const FinTechPartnerList = () => {
const [partners, setPartners] = useState([]);
useEffect(() => {
const fetchPartners = async () => {
try {
const res = await fintechApi.get('/fintech-partners');
setPartners(res.data.partners);
} catch (error) {
console.error(error);
toast.error('Failed to fetch FinTech partners');
}
};
fetchPartners();
}, []);
return (
<Grid container spacing={2} sx={{ padding: 2 }}>
{partners.map((partner) => (
<Grid item xs={12} md={6} key={partner._id}>
<FinTechPartnerItem partner={partner} setPartners={setPartners} />
</Grid>
))}
</Grid>
);
};
export default FinTechPartnerList;
o src/components/FinTechPartner/FinTechPartnerItem.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/FinTechPartner/FinTechPartnerItem.js
import React, { useState } from 'react';
import { Card, CardContent, Typography, Button, Dialog, DialogTitle, DialogContent, DialogActions, TextField } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
const FinTechPartnerItem = ({ partner, setPartners }) => {
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState({
name: partner.name,
apiKey: partner.apiKey,
apiSecret: partner.apiSecret,
endpoint: partner.endpoint,
status: partner.status,
});
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleUpdate = async () => {
try {
const res = await fintechApi.put(`/fintech-partners/${partner._id}`, formData);
toast.success('FinTech partner updated successfully');
setPartners((prev) =>
prev.map((p) => (p._id === partner._id ? res.data.partner : p))
);
handleClose();
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to update partner');
}
};
const handleRemove = async () => {
try {
await fintechApi.delete(`/fintech-partners/${partner._id}`);
toast.success('FinTech partner removed successfully');
setPartners((prev) => prev.filter((p) => p._id !== partner._id));
} catch (error) {
console.error(error);
toast.error(error.response?.data?.message || 'Failed to remove partner');
}
};
return (
<>
<Card>
<CardContent>
<Typography variant="h6">{partner.name}</Typography>
<Typography variant="body2">API Key: {partner.apiKey}</Typography>
<Typography variant="body2">API Secret: {partner.apiSecret}</Typography>
<Typography variant="body2">Endpoint: {partner.endpoint}</Typography>
<Typography variant="body2">Status: {partner.status}</Typography>
<Button variant="outlined" color="primary" sx={{ mt: 2, mr: 1 }} onClick={handleOpen}>
Edit
</Button>
<Button variant="outlined" color="secondary" sx={{ mt: 2 }} onClick={handleRemove}>
Remove
</Button>
</CardContent>
</Card>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Edit FinTech Partner</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Partner Name"
name="name"
fullWidth
value={formData.name}
onChange={handleChange}
/>
<TextField
margin="dense"
label="API Key"
name="apiKey"
fullWidth
value={formData.apiKey}
onChange={handleChange}
/>
<TextField
margin="dense"
label="API Secret"
name="apiSecret"
fullWidth
value={formData.apiSecret}
onChange={handleChange}
/>
<TextField
margin="dense"
label="API Endpoint"
name="endpoint"
fullWidth
value={formData.endpoint}
onChange={handleChange}
/>
<TextField
margin="dense"
label="Status"
name="status"
select
SelectProps={{
native: true,
}}
fullWidth
value={formData.status}
onChange={handleChange}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</TextField>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleUpdate} variant="contained" color="primary">
Save
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default FinTechPartnerItem;
8. Implementing Transaction Components:
o src/components/Transaction/TransactionList.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/Transaction/TransactionList.js
import React, { useEffect, useState } from 'react';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography } from '@mui/material';
import fintechApi from '../../services/fintechApi';
import { toast } from 'react-toastify';
const TransactionList = () => {
const [transactions, setTransactions] = useState([]);
useEffect(() => {
const fetchTransactions = async () => {
try {
const res = await fintechApi.get('/transactions'); // Assuming endpoint exists
setTransactions(res.data.transactions);
} catch (error) {
console.error(error);
toast.error('Failed to fetch transactions');
}
};
fetchTransactions();
}, []);
return (
<TableContainer component={Paper} sx={{ maxWidth: 800, margin: 'auto', mt: 4 }}>
<Typography variant="h6" sx={{ padding: 2 }}>
Recent Transactions
</Typography>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Description</TableCell>
<TableCell>Amount (USD)</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{transactions.map((txn) => (
<TableRow key={txn._id}>
<TableCell>{new Date(txn.date).toLocaleDateString()}</TableCell>
<TableCell>{txn.description}</TableCell>
<TableCell>{txn.amount}</TableCell>
<TableCell>{txn.status}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
export default TransactionList;
9. Implementing FinTech Pages:
o src/pages/AddDebitCard.js:
javascript
Copy code
// frontend/fintech-dashboard/src/pages/AddDebitCard.js
import React from 'react';
import DebitCardForm from '../components/DebitCard/DebitCardForm';
import DebitCardList from '../components/DebitCard/DebitCardList';
const AddDebitCard = () => {
return (
<>
<DebitCardForm />
<DebitCardList />
</>
);
};
export default AddDebitCard;
o src/pages/ManageFinTechPartners.js:
javascript
Copy code
// frontend/fintech-dashboard/src/pages/ManageFinTechPartners.js
import React from 'react';
import FinTechPartnerForm from '../components/FinTechPartner/FinTechPartnerForm';
import FinTechPartnerList from '../components/FinTechPartner/FinTechPartnerList';
const ManageFinTechPartners = () => {
return (
<>
<FinTechPartnerForm />
<FinTechPartnerList />
</>
);
};
export default ManageFinTechPartners;
o src/pages/Transactions.js:
javascript
Copy code
// frontend/fintech-dashboard/src/pages/Transactions.js
import React from 'react';
import TransactionList from '../components/Transaction/TransactionList';
const Transactions = () => {
return <TransactionList />;
};
export default Transactions;
10. Updating Routing in App.js:
o src/App.js:
javascript
Copy code
// frontend/fintech-dashboard/src/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import AddDebitCard from './pages/AddDebitCard';
import ManageFinTechPartners from './pages/ManageFinTechPartners';
import Transactions from './pages/Transactions';
import Navbar from './components/Navbar'; // Assuming a Navbar component exists
const App = () => {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Transactions />} />
<Route path="/add-debit-card" element={<AddDebitCard />} />
<Route path="/manage-fintech-partners" element={<ManageFinTechPartners />} />
</Routes>
<ToastContainer />
</Router>
);
};
export default App;
11. Implementing the Navbar (Optional):
o src/components/Navbar.js:
javascript
Copy code
// frontend/fintech-dashboard/src/components/Navbar.js
import React from 'react';
import { AppBar, Toolbar, Typography, Button } from '@mui/material';
import { Link } from 'react-router-dom';
const Navbar = () => {
const handleLogout = () => {
localStorage.removeItem('token');
window.location.reload();
};
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
FinTech Dashboard
</Typography>
<Button color="inherit" component={Link} to="/">
Transactions
</Button>
<Button color="inherit" component={Link} to="/add-debit-card">
Debit Cards
</Button>
<Button color="inherit" component={Link} to="/manage-fintech-partners">
FinTech Partners
</Button>
<Button color="inherit" onClick={handleLogout}>
Logout
</Button>
</Toolbar>
</AppBar>
);
};
export default Navbar;
12. Implementing Stripe Integration (Optional for Advanced Features):
o Ensure that your backend is set up to handle Stripe webhooks and that your frontend includes Stripe Elements for secure card data handling.
13. Testing the FinTech Service:
o Run Backend Tests:
bash
Copy code
cd fintech-service
npm test
o Run Frontend Tests:
bash
Copy code
cd frontend/fintech-dashboard
npm test
(Implement additional tests for frontend components as needed.)
14. Update Docker Compose Configuration:
o Add the FinTech Service and Frontend to docker-compose.yml:
yaml
Copy code
version: '3.8'
services:
mongodb:
image: mongo:5.0
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
rabbitmq:
image: rabbitmq:3-management
restart: always
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
# ... Other backend services
fintech-service:
build: ./fintech-service
restart: always
ports:
- "5014:5014"
environment:
- PORT=5014
- NODE_ENV=development
- MONGODB_URI=mongodb://root:example@mongodb:27017/fintechdb?authSource=admin
- JWT_SECRET=your_jwt_secret_key
- STRIPE_SECRET_KEY=your_stripe_secret_key
- PLAID_CLIENT_ID=your_plaid_client_id
- PLAID_SECRET=your_plaid_secret
- PLAID_ENV=sandbox
depends_on:
- mongodb
- rabbitmq
frontend-fintech:
build: ./frontend/fintech-dashboard
restart: always
expose:
- "3005"
environment:
- REACT_APP_API_BASE_URL=http://fintech-service:5014/api
- REACT_APP_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
depends_on:
- fintech-service
# ... Other frontend services
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/sites-available:/etc/nginx/sites-available
- ./nginx/sites-enabled:/etc/nginx/sites-enabled
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- frontend
- shop-frontend
- blog-frontend
- fundraising-frontend
- quick-store-frontend
- frontend-fintech
# ... Other services
volumes:
mongo-data:
15. Updating Nginx Configuration:
o Create Server Block for FinTech Dashboard Subdomain (fintech.ibrandidit.com):
/etc/nginx/sites-available/fintech.ibrandidit.com.conf:
nginx
Copy code
server {
listen 80;
server_name fintech.ibrandidit.com;
location / {
proxy_pass http://frontend-fintech:3005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name fintech.ibrandidit.com;
ssl_certificate /etc/letsencrypt/live/fintech.ibrandidit.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fintech.ibrandidit.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://frontend-fintech:3005;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on;
gzip_types text/plain application/json application/javascript text/css application/xml text/javascript;
gzip_proxied any;
gzip_vary on;
}
o Enable the Server Block:
bash
Copy code
sudo ln -s /etc/nginx/sites-available/fintech.ibrandidit.com.conf /etc/nginx/sites-enabled/
o Obtain SSL Certificate for FinTech Subdomain:
bash
Copy code
sudo certbot --nginx -d fintech.ibrandidit.com
o Reload Nginx:
bash
Copy code
sudo systemctl reload nginx
16. Finalizing Docker Compose for FinTech Frontend:
o Ensure the FinTech Frontend Dockerfile is correctly set up.
o frontend/fintech-dashboard/Dockerfile:
dockerfile
Copy code
# frontend/fintech-dashboard/Dockerfile
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
# Build the app
RUN npm run build
# Install serve
RUN npm install -g serve
# Expose port
EXPOSE 3005
# Start the app
CMD ["serve", "-s", "build", "-l", "3005"]
o Rebuild and Restart Docker Containers:
bash
Copy code
docker-compose up --build -d
Ensure all services are up and running:
bash
Copy code
docker-compose ps
________________________________________
4. Documentation and Usage
4.1. Running the BaaS Integration Services Locally
1. Ensure All Services Are Running:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
2. Access Frontend Applications:
o FinTech Dashboard: https://fintech.ibrandidit.com
o Other Subdomains:
Primary Frontend: https://ibrandidit.com
Shop Frontend: https://shop.ibrandidit.com
Blog Frontend: https://blog.ibrandidit.com
Fundraising Frontend: https://fundraising.ibrandidit.com
Quick Store Frontend: https://quickstore.ibrandidit.com
4.2. Interacting with FinTech Features
1. Adding a Debit Card
• Navigate to: https://fintech.ibrandidit.com/add-debit-card
• Process:
o Enter debit card details securely using Stripe Elements.
o Submit the form to add the card.
• Result: Debit card is added and listed under the user's debit cards.
2. Managing Debit Cards
• View Debit Cards:
o Debit cards are listed on the same page or a dedicated section.
o Details like brand, last four digits, and expiration date are displayed.
• Remove Debit Cards:
o Click on Remove Card to delete a debit card.
o Confirmation and success messages are displayed upon removal.
3. Managing FinTech Partners
• Navigate to: https://fintech.ibrandidit.com/manage-fintech-partners
• Process:
o Add FinTech Partner:
Fill out the form with partner details like name, API key, API secret, and endpoint.
Submit to add the partner.
o Edit FinTech Partner:
Click on Edit to modify partner details.
o Remove FinTech Partner:
Click on Remove to delete a partner.
4. Viewing Transactions
• Navigate to: https://fintech.ibrandidit.com/
• View List:
o Recent transactions are displayed in a table format.
o Details include date, description, amount, and status.
4.3. API Documentation
Access the Swagger UI for detailed API documentation of each backend service.
• FinTech Service: https://fintech.ibrandidit.com/api-docs
• Other Services:
o Auth Service: https://ibrandidit.com/api-docs
o Order Service: https://ibrandidit.com/api-docs
o Payment Service: https://ibrandidit.com/api-docs
o Shipping Service: https://ibrandidit.com/api-docs
o AI Service: https://ibrandidit.com/api-docs
o Product Customization Service: https://ibrandidit.com/api-docs
o Affiliate & Distributor Management Service: https://ibrandidit.com/api-docs
o Inventory Management Service: https://ibrandidit.com/api-docs
o Fundraising Service: https://fundraising.ibrandidit.com/api-docs
o Quick Store Service: https://quickstore.ibrandidit.com/api-docs
4.4. Testing
Backend Testing
1. Run All Backend Service Tests:
bash
Copy code
# For each backend service, run:
cd path/to/service
npm test
o Ensure all tests pass, verifying the functionality of each API endpoint.
Frontend Testing
1. Run All Frontend Tests:
bash
Copy code
# For each frontend application, run:
cd path/to/frontend
npm test
o Ensure that all tests pass, especially those related to debit card and FinTech partnership functionalities.
4.5. Linting and Formatting
Maintain code quality and consistency using ESLint and Prettier.
1. Run Linting:
bash
Copy code
cd path/to/service-or-frontend
npm run lint
2. Run Formatting:
bash
Copy code
cd path/to/service-or-frontend
npm run format
4.6. Deployment
1. Ensure All Services Are Containerized:
o All backend and frontend services should have Dockerfiles.
2. Build and Deploy with Docker Compose:
bash
Copy code
cd path/to/ibrandidit-microservices/
docker-compose up --build -d
3. Verify Deployment:
o Access each domain and subdomain to ensure they're operational and serving the correct frontend applications.
4. Monitor Services:
o Use Docker logs to monitor service health.
bash
Copy code
docker-compose logs -f
4.7. Contribution Guidelines
1. Fork the Repository:
Create a personal fork of the project repository.
2. Create a Feature Branch:
Develop your feature or bug fix in a separate branch.
bash
Copy code
git checkout -b feature/fintech-integration
3. Commit Changes:
Write clear and concise commit messages.
bash
Copy code
git commit -m "feat: integrate debit card and FinTech partnership features"
4. Push to Fork and Create Pull Request:
bash
Copy code
git push origin feature/fintech-integration
o Submit a pull request from your fork to the main repository for review.
________________________________________
5. Complete Code and Configuration Files
5.1. FinTech Service
• Directory Structure:
bash
Copy code
fintech-service/
├── controllers/
│ ├── cardController.js
│ ├── fintechController.js
│ └── webhookController.js
├── models/
│ ├── DebitCard.js
│ └── FinTechPartner.js
├── routes/
│ ├── cardRoutes.js
│ ├── fintechRoutes.js
│ └── webhookRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ ├── cardController.test.js
│ └── fintechController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
• Key Files:
o models/DebitCard.js
(As provided in Implementing the DebitCard Model)
o models/FinTechPartner.js
(As provided in Implementing the FinTechPartner Model)
o controllers/cardController.js
(As provided in Implementing the Card Controller)
o controllers/fintechController.js
(As provided in Implementing the FinTech Controller)
o controllers/webhookController.js
(As provided in Implementing Stripe Webhooks)
o routes/cardRoutes.js
(As provided in Implementing the Card Routes)
o routes/fintechRoutes.js
(As provided in Implementing the FinTech Routes)
o routes/webhookRoutes.js
(As provided in Implementing Stripe Webhooks)
o middleware/authenticate.js
(As provided in Authentication Middleware)
o middleware/errorHandler.js
(As provided in Error Handling Middleware)
o utils/logger.js
(As provided in Logger Utility)
o index.js
(As provided in FinTech Service Main Application File)
o tests/cardController.test.js
(As provided in Testing the Card Controller)
o tests/fintechController.test.js
(As provided in Testing the FinTech Controller)
5.2. FinTech Frontend Service
• Directory Structure:
bash
Copy code
frontend/fintech-dashboard/
├── src/
│ ├── components/
│ │ ├── DebitCard/
│ │ │ ├── DebitCardForm.js
│ │ │ ├── DebitCardList.js
│ │ │ └── DebitCardItem.js
│ │ ├── FinTechPartner/
│ │ │ ├── FinTechPartnerForm.js
│ │ │ ├── FinTechPartnerList.js
│ │ │ └── FinTechPartnerItem.js
│ │ └── Transaction/
│ │ └── TransactionList.js
│ ├── pages/
│ │ ├── AddDebitCard.js
│ │ ├── ManageFinTechPartners.js
│ │ └── Transactions.js
│ ├── services/
│ │ └── fintechApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
• Key Files:
o src/services/fintechApi.js
(As provided in Implementing the FinTech API Service)
o src/components/DebitCard/DebitCardForm.js
(As provided in Implementing Debit Card Components)
o src/components/DebitCard/DebitCardList.js
(As provided in Implementing Debit Card Components)
o src/components/FinTechPartner/FinTechPartnerForm.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerList.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerItem.js
(As provided in Implementing FinTech Partner Components)
o src/components/Transaction/TransactionList.js
(As provided in Implementing Transaction Components)
o src/pages/AddDebitCard.js
(As provided in Implementing FinTech Pages)
o src/pages/ManageFinTechPartners.js
(As provided in Implementing FinTech Pages)
o src/pages/Transactions.js
(As provided in Implementing FinTech Pages)
o src/App.js
(As provided in Updating Routing in App.js)
o src/components/Navbar.js
(As provided in Implementing the Navbar)
5.3. Nginx Configuration Files
(As detailed in Updating Nginx Configuration)
5.4. Docker Compose File
(As detailed in Update Docker Compose Configuration)
5.5. Frontend Applications
(As detailed in Frontend Implementation)
5.6. Backend Services
(As detailed in Backend Implementation)
________________________________________
6. Troubleshooting Tips
1. Nginx Not Routing Correctly:
o Problem: Requests to FinTech subdomains are not reaching the intended frontend service.
o Solution:
Verify that the server blocks in Nginx are correctly configured.
Ensure that Docker Compose service names (frontend-fintech) match the proxy pass URLs in Nginx configuration.
Check Nginx logs for any errors:
bash
Copy code
sudo tail -f /var/log/nginx/error.log
2. SSL Certificate Issues:
o Problem: Browsers show certificate warnings or errors.
o Solution:
Ensure that SSL certificates are correctly installed and renewed.
Verify that the certificate paths in the Nginx configuration are accurate.
Use sudo certbot renew to manually renew certificates if automatic renewal fails.
3. Stripe Integration Issues:
o Problem: Debit card payments fail or do not process correctly.
o Solution:
Ensure that Stripe API keys (STRIPE_SECRET_KEY and REACT_APP_STRIPE_PUBLISHABLE_KEY) are correctly set in the .env files.
Verify that the Stripe account is in the correct mode (sandbox for testing, live for production).
Check Stripe dashboard for any error logs or failed transactions.
Ensure that the frontend uses Stripe Elements correctly for secure card data handling.
4. Plaid Integration Issues (if implemented):
o Problem: User bank account linking fails or data retrieval issues.
o Solution:
Ensure that Plaid API keys (PLAID_CLIENT_ID, PLAID_SECRET, PLAID_ENV) are correctly set in the .env files.
Verify that the Plaid account is in the correct environment (sandbox for testing, development or production for live data).
Check Plaid dashboard for any error logs or failed integrations.
Ensure that the frontend correctly handles Plaid Link for secure bank account linking.
5. Authentication Errors:
o Problem: Users receive 401 Unauthorized errors when accessing FinTech APIs.
o Solution:
Ensure that JWT tokens are correctly passed in the Authorization header.
Verify that the authentication middleware is correctly implemented in backend services.
Check that all services share the same JWT_SECRET.
6. Database Connection Issues:
o Problem: FinTech service cannot connect to MongoDB.
o Solution:
Ensure that MongoDB is running and accessible.
Verify the MONGODB_URI in the .env files.
Check network configurations and firewall settings.
7. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
8. Resource Conflicts:
o Problem: Multiple services are trying to use the same port.
o Solution:
Ensure that each service in Docker Compose has unique port mappings.
Use internal port exposure (expose) and let Nginx handle external port assignments.
9. Form Validation Errors:
o Problem: Users cannot submit forms on FinTech dashboard due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
10. Performance Bottlenecks:
o Problem: FinTech features are loading slowly.
o Solution:
Enable caching in Nginx.
Optimize frontend assets (minification, image optimization).
Scale services horizontally if necessary.
________________________________________
7. Additional Tools and Best Practices
1. Rate Limiting:
o Prevent abuse by limiting the number of requests per user/IP.
o Already implemented using express-rate-limit in backend services.
2. Caching:
o Implement caching for frequently accessed data to reduce database load and improve response times.
o Use Nginx caching or in-memory caching solutions like Redis if necessary.
3. Logging:
o Implement comprehensive logging to monitor service performance and debug issues.
o Use Winston for structured logging in backend services.
4. Security:
o Input Validation: Use libraries like express-validator to validate and sanitize user inputs.
o Helmet: Already implemented to set secure HTTP headers.
o HTTPS: Ensure all communications are over HTTPS in production environments.
5. Error Handling:
o Implement centralized error handling to provide meaningful feedback to users and maintain application stability.
o Use error-handling middleware in backend services.
6. Responsive Design:
o Ensure that FinTech dashboard is responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
7. Version Control:
o Commit changes regularly with clear messages.
o Use feature branches for new functionalities.
o Merge changes via pull requests with code reviews.
8. Documentation Maintenance:
o Ensure that all changes are reflected in the documentation.
o Encourage team members to contribute to documentation improvements.
________________________________________
8. Complete Code and Configuration Files
8.1. FinTech Service
• Directory Structure:
bash
Copy code
fintech-service/
├── controllers/
│ ├── cardController.js
│ ├── fintechController.js
│ └── webhookController.js
├── models/
│ ├── DebitCard.js
│ └── FinTechPartner.js
├── routes/
│ ├── cardRoutes.js
│ ├── fintechRoutes.js
│ └── webhookRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ ├── cardController.test.js
│ └── fintechController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
• Key Files:
o models/DebitCard.js
(As provided in Implementing the DebitCard Model)
o models/FinTechPartner.js
(As provided in Implementing the FinTechPartner Model)
o controllers/cardController.js
(As provided in Implementing the Card Controller)
o controllers/fintechController.js
(As provided in Implementing the FinTech Controller)
o controllers/webhookController.js
(As provided in Implementing Stripe Webhooks)
o routes/cardRoutes.js
(As provided in Implementing the Card Routes)
o routes/fintechRoutes.js
(As provided in Implementing the FinTech Routes)
o routes/webhookRoutes.js
(As provided in Implementing Stripe Webhooks)
o middleware/authenticate.js
(As provided in Authentication Middleware)
o middleware/errorHandler.js
(As provided in Error Handling Middleware)
o utils/logger.js
(As provided in Logger Utility)
o index.js
(As provided in FinTech Service Main Application File)
o tests/cardController.test.js
(As provided in Testing the Card Controller)
o tests/fintechController.test.js
(As provided in Testing the FinTech Controller)
8.2. FinTech Frontend Service
• Directory Structure:
bash
Copy code
frontend/fintech-dashboard/
├── src/
│ ├── components/
│ │ ├── DebitCard/
│ │ │ ├── DebitCardForm.js
│ │ │ ├── DebitCardList.js
│ │ │ └── DebitCardItem.js
│ │ ├── FinTechPartner/
│ │ │ ├── FinTechPartnerForm.js
│ │ │ ├── FinTechPartnerList.js
│ │ │ └── FinTechPartnerItem.js
│ │ └── Transaction/
│ │ └── TransactionList.js
│ ├── pages/
│ │ ├── AddDebitCard.js
│ │ ├── ManageFinTechPartners.js
│ │ └── Transactions.js
│ ├── services/
│ │ └── fintechApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
• Key Files:
o src/services/fintechApi.js
(As provided in Implementing the FinTech API Service)
o src/components/DebitCard/DebitCardForm.js
(As provided in Implementing Debit Card Components)
o src/components/DebitCard/DebitCardList.js
(As provided in Implementing Debit Card Components)
o src/components/FinTechPartner/FinTechPartnerForm.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerList.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerItem.js
(As provided in Implementing FinTech Partner Components)
o src/components/Transaction/TransactionList.js
(As provided in Implementing Transaction Components)
o src/pages/AddDebitCard.js
(As provided in Implementing FinTech Pages)
o src/pages/ManageFinTechPartners.js
(As provided in Implementing FinTech Pages)
o src/pages/Transactions.js
(As provided in Implementing FinTech Pages)
o src/App.js
(As provided in Updating Routing in App.js)
o src/components/Navbar.js
(As provided in Implementing the Navbar)
8.3. Nginx Configuration Files
(As detailed in Updating Nginx Configuration)
8.4. Docker Compose File
(As detailed in Update Docker Compose Configuration)
8.5. Frontend Applications
(As detailed in Frontend Implementation)
8.6. Backend Services
(As detailed in Backend Implementation)
________________________________________
9. Troubleshooting Tips
1. Nginx Not Routing Correctly:
o Problem: Requests to FinTech subdomains are not reaching the intended frontend service.
o Solution:
Verify that the server blocks in Nginx are correctly configured.
Ensure that Docker Compose service names (frontend-fintech) match the proxy pass URLs in Nginx configuration.
Check Nginx logs for any errors:
bash
Copy code
sudo tail -f /var/log/nginx/error.log
2. SSL Certificate Issues:
o Problem: Browsers show certificate warnings or errors.
o Solution:
Ensure that SSL certificates are correctly installed and renewed.
Verify that the certificate paths in the Nginx configuration are accurate.
Use sudo certbot renew to manually renew certificates if automatic renewal fails.
3. Stripe Integration Issues:
o Problem: Debit card payments fail or do not process correctly.
o Solution:
Ensure that Stripe API keys (STRIPE_SECRET_KEY and REACT_APP_STRIPE_PUBLISHABLE_KEY) are correctly set in the .env files.
Verify that the Stripe account is in the correct mode (sandbox for testing, live for production).
Check Stripe dashboard for any error logs or failed transactions.
Ensure that the frontend uses Stripe Elements correctly for secure card data handling.
4. Plaid Integration Issues (if implemented):
o Problem: User bank account linking fails or data retrieval issues.
o Solution:
Ensure that Plaid API keys (PLAID_CLIENT_ID, PLAID_SECRET, PLAID_ENV) are correctly set in the .env files.
Verify that the Plaid account is in the correct environment (sandbox for testing, development or production for live data).
Check Plaid dashboard for any error logs or failed integrations.
Ensure that the frontend correctly handles Plaid Link for secure bank account linking.
5. Authentication Errors:
o Problem: Users receive 401 Unauthorized errors when accessing FinTech APIs.
o Solution:
Ensure that JWT tokens are correctly passed in the Authorization header.
Verify that the authentication middleware is correctly implemented in backend services.
Check that all services share the same JWT_SECRET.
6. Database Connection Issues:
o Problem: FinTech service cannot connect to MongoDB.
o Solution:
Ensure that MongoDB is running and accessible.
Verify the MONGODB_URI in the .env files.
Check network configurations and firewall settings.
7. Docker Networking Issues:
o Problem: Frontend cannot reach backend services when running in Docker.
o Solution:
Use service names defined in docker-compose.yml instead of localhost for inter-service communication.
Verify that all services are part of the same Docker network.
Check for any firewall or network restrictions.
8. Resource Conflicts:
o Problem: Multiple services are trying to use the same port.
o Solution:
Ensure that each service in Docker Compose has unique port mappings.
Use internal port exposure (expose) and let Nginx handle external port assignments.
9. Form Validation Errors:
o Problem: Users cannot submit forms on FinTech dashboard due to validation errors.
o Solution:
Ensure that all required fields are filled out correctly.
Review Yup validation schemas for any inconsistencies.
Provide clear error messages to guide users in correcting inputs.
10. Performance Bottlenecks:
o Problem: FinTech features are loading slowly.
o Solution:
Enable caching in Nginx.
Optimize frontend assets (minification, image optimization).
Scale services horizontally if necessary.
________________________________________
10. Additional Tools and Best Practices
1. Rate Limiting:
o Prevent abuse by limiting the number of requests per user/IP.
o Already implemented using express-rate-limit in backend services.
2. Caching:
o Implement caching for frequently accessed data to reduce database load and improve response times.
o Use Nginx caching or in-memory caching solutions like Redis if necessary.
3. Logging:
o Implement comprehensive logging to monitor service performance and debug issues.
o Use Winston for structured logging in backend services.
4. Security:
o Input Validation: Use libraries like express-validator to validate and sanitize user inputs.
o Helmet: Already implemented to set secure HTTP headers.
o HTTPS: Ensure all communications are over HTTPS in production environments.
5. Error Handling:
o Implement centralized error handling to provide meaningful feedback to users and maintain application stability.
o Use error-handling middleware in backend services.
6. Responsive Design:
o Ensure that FinTech dashboard is responsive and accessible across various devices and screen sizes.
o Utilize Material-UI's Grid system and responsive components.
7. Version Control:
o Commit changes regularly with clear messages.
o Use feature branches for new functionalities.
o Merge changes via pull requests with code reviews.
8. Documentation Maintenance:
o Ensure that all changes are reflected in the documentation.
o Encourage team members to contribute to documentation improvements.
________________________________________
11. Complete Code and Configuration Files
11.1. FinTech Service
• Directory Structure:
bash
Copy code
fintech-service/
├── controllers/
│ ├── cardController.js
│ ├── fintechController.js
│ └── webhookController.js
├── models/
│ ├── DebitCard.js
│ └── FinTechPartner.js
├── routes/
│ ├── cardRoutes.js
│ ├── fintechRoutes.js
│ └── webhookRoutes.js
├── middleware/
│ ├── authenticate.js
│ └── errorHandler.js
├── tests/
│ ├── cardController.test.js
│ └── fintechController.test.js
├── utils/
│ └── logger.js
├── .env
├── .gitignore
├── index.js
├── package.json
└── README.md
• Key Files:
o models/DebitCard.js
javascript
Copy code
// fintech-service/models/DebitCard.js
const mongoose = require('mongoose');
const DebitCardSchema = new mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
stripeCardId: {
type: String,
required: true,
},
brand: {
type: String,
required: true,
},
last4: {
type: String,
required: true,
},
expMonth: {
type: Number,
required: true,
},
expYear: {
type: Number,
required: true,
},
},
{ timestamps: true }
);
module.exports = mongoose.model('DebitCard', DebitCardSchema);
o models/FinTechPartner.js
javascript
Copy code
// fintech-service/models/FinTechPartner.js
const mongoose = require('mongoose');
const FinTechPartnerSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
apiKey: {
type: String,
required: true,
},
apiSecret: {
type: String,
required: true,
},
endpoint: {
type: String,
required: true,
},
status: {
type: String,
enum: ['active', 'inactive'],
default: 'active',
},
},
{ timestamps: true }
);
module.exports = mongoose.model('FinTechPartner', FinTechPartnerSchema);
o controllers/cardController.js
(As provided in Implementing the Card Controller)
o controllers/fintechController.js
(As provided in Implementing the FinTech Controller)
o controllers/webhookController.js
(As provided in Implementing Stripe Webhooks)
o routes/cardRoutes.js
(As provided in Implementing the Card Routes)
o routes/fintechRoutes.js
(As provided in Implementing the FinTech Routes)
o routes/webhookRoutes.js
(As provided in Implementing Stripe Webhooks)
o middleware/authenticate.js
(As provided in Authentication Middleware)
o middleware/errorHandler.js
(As provided in Error Handling Middleware)
o utils/logger.js
(As provided in Logger Utility)
o index.js
(As provided in FinTech Service Main Application File)
o tests/cardController.test.js
(As provided in Testing the Card Controller)
o tests/fintechController.test.js
(As provided in Testing the FinTech Controller)
11.2. FinTech Frontend Service
• Directory Structure:
bash
Copy code
frontend/fintech-dashboard/
├── src/
│ ├── components/
│ │ ├── DebitCard/
│ │ │ ├── DebitCardForm.js
│ │ │ ├── DebitCardList.js
│ │ │ └── DebitCardItem.js
│ │ ├── FinTechPartner/
│ │ │ ├── FinTechPartnerForm.js
│ │ │ ├── FinTechPartnerList.js
│ │ │ └── FinTechPartnerItem.js
│ │ └── Transaction/
│ │ └── TransactionList.js
│ ├── pages/
│ │ ├── AddDebitCard.js
│ │ ├── ManageFinTechPartners.js
│ │ └── Transactions.js
│ ├── services/
│ │ └── fintechApi.js
│ ├── App.js
│ ├── index.js
│ └── ...other files
├── .env
├── package.json
└── README.md
• Key Files:
o src/services/fintechApi.js
(As provided in Implementing the FinTech API Service)
o src/components/DebitCard/DebitCardForm.js
(As provided in Implementing Debit Card Components)
o src/components/DebitCard/DebitCardList.js
(As provided in Implementing Debit Card Components)
o src/components/FinTechPartner/FinTechPartnerForm.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerList.js
(As provided in Implementing FinTech Partner Components)
o src/components/FinTechPartner/FinTechPartnerItem.js
(As provided in Implementing FinTech Partner Components)
o src/components/Transaction/TransactionList.js
(As provided in Implementing Transaction Components)
o src/pages/AddDebitCard.js
(As provided in Implementing FinTech Pages)
o src/pages/ManageFinTechPartners.js
(As provided in Implementing FinTech Pages)
o src/pages/Transactions.js
(As provided in Implementing FinTech Pages)
o src/App.js
(As provided in Updating Routing in App.js)
o src/components/Navbar.js
(As provided in Implementing the Navbar)
11.3. Nginx Configuration Files
(As detailed in Updating Nginx Configuration)
11.4. Docker Compose File
(As detailed in Update Docker Compose Configuration)
11.5. Frontend Applications
(As detailed in Frontend Implementation)
11.6. Backend Services
(As detailed in Backend Implementation)
________________________________________
12. Conclusion
By meticulously following this comprehensive guide for Stage 20: BaaS Integration for Debit Card and FinTech Partnership, you have successfully integrated robust financial services into your eCommerce platform. This final stage not only enhances the platform's payment capabilities but also establishes strong partnerships with FinTech providers, paving the way for advanced financial functionalities and increased user trust.
Next Steps:
• Maintenance and Monitoring: Regularly monitor service performance, security, and reliability.
• User Feedback: Gather and incorporate user feedback to continuously improve financial services.
• Scalability: Plan for scaling services as the platform grows to handle increased transactions and partnerships.
• Continuous Integration/Continuous Deployment (CI/CD): Implement CI/CD pipelines for automated testing and deployment.
________________________________________
Disclaimer: This guide provides a foundational setup for Stage 20: BaaS Integration for Debit Card and FinTech Partnership within a microservices architecture. Depending on project complexity and specific requirements, further optimizations and configurations may be necessary, such as implementing advanced security measures, integrating with additional third-party services for enhanced functionalities, or customizing features to cater to specific user needs.
________________________________________
By completing this final stage, your eCommerce platform is now equipped with comprehensive financial services, offering users a seamless and secure shopping experience while providing you with advanced tools to manage and scale your business effectively.