I had to save a canvas drawing but my POST request is blocked by CORS problem

24 views Asked by At

Introduction

I have researched this CORS problem. Chat GPT is going round in circles and nothing it has advised me to do has worked.

The functionality I've coded is quite simple, and you can even reproduce it to try for yourself: it's a simple canvas on which the user can draw a picture. The user then has the option of sending the drawing. It's on this last point that I'm stuck.

However, I think I'm doing the right thing: on the server side, I'm using the "cors" module and I'm clearly authorizing all origins (I know it's not safe, but I'm still trying).

Context

On the front-end, this is a React 18 application. On the back-end, I've built a NodesJS/Express API which is hosted in a VPS running on a Nginx web server.

The drawing is physically saved as a png in a folder on the server (initially only for viewing the image, but I could just as easily remove this part of the code) and in a CouchDB database in Base64: this is a conversion of the image into a very long character string that takes up very little space and which the browser itself can then reconstruct so that it's visible.

In my React component, I use fetch, which is a simple HTTPS POST request that saves the image in base64 in my database (called "usersdrawings").

Problem: when sending the drawing, I systematically get the following error message in my browser's console:

Cross-origin redirection to https://*********.fun/drawings/ denied by Cross-Origin >Resource Sharing policy: Origin http://localhost:3000 is not allowed by Access->Control-Allow-Origin. Status code: 301

So there are 3 important things to consider to solve the problem:

  • My React component (called DrawingCanvas)
  • My NodeJS/Express API script
  • The Nginx configuration file for the domain name that allows me to connect to couchDB using HTTPS.
  • 1-React component

    To start with, here's my React component (the interesting thing is the handleSendDrawing function, which is triggered when the user clicks on the button to send his drawing):

    import React, { useState, useRef, useEffect } from 'react';
    import cursorImage from './cursor7.png'; 
    import FalshBtn2 from '../FalshBtn2';
    
    const DrawingCanvas = () => {
      const canvasRef = useRef(null);
      const [drawing, setDrawing] = useState(false);
      const [currentColor, setCurrentColor] = useState('#000000');
      const [canvasWidth, setCanvasWidth] = useState(740);
      const [canvasHeight, setCanvasHeight] = useState(555);
    
      useEffect(() => {
        const interval = setInterval(() => {
          let randomColor;
          do {
            randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
          } while (randomColor === '#ffffff');
          setCurrentColor(randomColor);
        }, 500);
      
        return () => clearInterval(interval);
      }, []);
    
      useEffect(() => {
        const canvas = canvasRef.current;
        const context = canvas.getContext('2d');
        context.lineWidth = 20;
        context.lineCap = 'round';
    
        const handleMouseDown = (event) => {
          setDrawing(true);
          drawPoint(event);
        };
    
        const handleMouseMove = (event) => {
          if (!drawing) return;
          drawPoint(event); 
        };
    
        const handleMouseUp = () => {
          setDrawing(false);
        };
    
        const drawPoint = (event) => {
          const canvas = canvasRef.current;
          const context = canvas.getContext('2d');
          const rect = canvas.getBoundingClientRect();
          const x = event.clientX - rect.left;
          const y = event.clientY - rect.top;
          context.fillStyle = currentColor;
          context.beginPath();
          context.arc(x, y, 15, 0, Math.PI * 2);
          context.fill();
        };
    
        canvas.addEventListener('mousedown', handleMouseDown);
        canvas.addEventListener('mousemove', handleMouseMove);
        canvas.addEventListener('mouseup', handleMouseUp);
    
        return () => {
          canvas.removeEventListener('mousedown', handleMouseDown);
          canvas.removeEventListener('mousemove', handleMouseMove);
          canvas.removeEventListener('mouseup', handleMouseUp);
        };
      }, [drawing, currentColor]);
    
      const handleSendDrawing = async () => {
        const canvas = canvasRef.current;
        const dataURL = canvas.toDataURL(); 
      
    
        const formData = new FormData();
        formData.append('drawing', dataURL);
      
        try {
          const response = await fetch('https://************.fun/drawings', {
            method: 'POST',
            body: formData
          });
      
          if (!response.ok) {
            throw new Error('Error');
          }
          const context = canvas.getContext('2d');
          context.clearRect(0, 0, canvas.width, canvas.height);
        } catch (error) {
          console.error("Error", error);
        }
      };
      return (
        <>
        <div className='lecadre'>
            <div className='container_canvas_bouton'>
                <div className='zoneArtistique'>
                    <canvas
                        ref={canvasRef}
                        width={canvasWidth}
                        height={canvasHeight}
                        className='dessiner'
                        style={{ cursor: `url(${cursorImage}) 10 10, auto` }}>
                    </canvas>
                </div>
            </div>
        </div>
        <div className="flex justify-center py-8">
            <FalshBtn2 color={"cyan"} texte={"envoyer"} handleClick={handleSendDrawing} />
        </div>
        </>
      );
    };
    export default DrawingCanvas;
    

    2-NodeJS/Express API

    On the back-end, here's the NodeJS/Express API script, which has a single route: /drawings:

    const express = require('express');
    const app = express();
    const fs = require('fs');
    const path = require('path');
    const logStream = fs.createWriteStream('logs.txt', { flags: 'a' });
    const nano = require('nano');
    const cors = require('cors');
    
    require('dotenv').config();
    // Middleware for debugging
    app.use((req, res, next) => {
        const logMessage = `[${new Date().toISOString()}] ${req.method} ${req.url}\n`;
        logStream.write(logMessage + '/n');
        next();
    });
    
    // Middleware CORS
    app.use(cors({
    origin: '*',
    methods: ['OPTIONS', 'GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization']
    }));
    
    // Middleware parse JSON
    app.use(express.json());
    
    // Middleware for parse datas POST
    app.use(express.urlencoded({ extended: false }));
    
    // error handle
    app.use((err, req, res, next) => {
        console.error(err.stack);
        const errorMessage = `[${new Date().toISOString()}] An error has occurred: ${err.stack}\n`;
        logStream.write(errorMessage + '\n');
        res.status(500).json({ message: 'An error has occurred.' });
    });
    
    // couchDB access
    const dbHost = process.env.DB_HOST_COUCHDB;
    const dbUser = process.env.DB_USER_COUCHDB;
    const dbPassword = process.env.DB_PSW_COUCHDB;
    const dbPort = process.env.DB_PRT_COUCHDB;
    
    // drawings folder
    const drawingsDirectory = path.join(__dirname, 'drawings');
    
    
    // couchDB connexion
    const nanoOptions = {
        url: `https://${dbUser}:${dbPassword}@${dbHost}:${dbPort}`,
        parseUrl: false
    };
    const db = nano(nanoOptions);
    
    //couchDB database
    const drawingsDB = db.use('usersdrawings');
    
    // Endpoint
    app.post('/drawings', async (req, res) => {
        const dataURL = req.body.drawing;
    
        // Vérify folder space
        const directorySize = getDirectorySize(drawingsDirectory);
        const maxDirectorySize = 2 * 1024 * 1024 * 1024; // 2 Go max
    
        if (directorySize >= maxDirectorySize) {
          return res.status(500).json({ error: 'Space of storage is full' });
        }
        
        // Create file png of drawing
        const fileName = `drawing_${Date.now()}.png`;
        const filePath = path.join(drawingsDirectory, fileName);
        
        fs.writeFile(filePath, dataURL.replace(/^data:image\/png;base64,/, ''), 'base64', async (err) => {
          if (err) {
            logStream.write('Error :' + err + '\n');
            return res.status(500).json({ error: 'Error' });
          }
          
          try {
            // save drawing in base64 in CouchDB database
            await drawingsDB.insert({ 
              drawing: dataURL,
              filePath: filePath
            });
            
            res.status(200).json({ message: 'Success !' });
            logStream.write('Drawing saved in couchDB !' + '\n');
          } catch (error) {
            logStream.write('Error during save process in couchDB' + error + '\n');
            res.status(500).json({ error: 'Error during save process in couchDB' });
          }
        });
      });
    
    // Function verify the size
    function getDirectorySize(directory) {
        let size = 0;
        const files = fs.readdirSync(directory);
      
        files.forEach(file =>{ 
          const filePath = path.join(directory, file);
          const stats = fs.statSync(filePath);
          size += stats.size;
        });
    
        logStream.write('Total folder size: ' + size + '\n');
        return size;
      }
    
    app.listen(3002, () => {
      logStream.write(`API in port 3002` + '\n');
      console.log('API running on port 3002');
    });
    

    3 - The service that makes the API accessible is active:

    ● my-api-usersdrawings.service - users drawings API
    Loaded: loaded (/etc/systemd/system/my-api-usersdrawings.service; enabled; vendor >preset: enabled)
    Active: active (running) since Sun 2024-03-24 04:51:21 CET; 2s ago
    Main PID: 3297242 (node)
    Tasks: 11 (limit: 2309)
    Memory: 16.8M
    CPU: 177ms
    CGroup: /system.slice/my-api-usersdrawings.service
    └─3297242 /usr/bin/node /opt/api-drawings/app.js
    Mar 24 04:51:21 ubuntu-s-1vcpu-2gb-intel-fra1-01 systemd[1]: Started users drawings API
    Mar 24 04:51:21 ubuntu-s-1vcpu-2gb-intel-fra1-01 node[3297242]: API running on port 3002

    4- Finally, here's the Nginx configuration file for the domain name (which has a valid SSL certificate).

    # domain name used to access couchDB in HTTPS
    server {
        listen 80;
        listen [::]:80;
        server_name *******************.fun;
        
        location /.well-known/acme-challenge/ {
            root /var/certbot;
        }
    
        location / {
            return 301 https://$host$request_uri;
        }
    }
    
    server {
           listen 443 ssl;
           listen [::]:443 ssl;
           server_name *******************.fun;
           ssl_certificate /etc/letsencrypt/live/*******************.fun/fullchain.pem;
           ssl_certificate_key /etc/letsencrypt/live/*******************.fun/privkey.pem;
           include /etc/letsencrypt/options-ssl-nginx.conf;
           ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
    # NodeJS/Express for users drawings running on port 3002
            location /drawings/ {
                    proxy_pass http://localhost:3002;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header Access-Control-Allow-Origin *;
    
            }
    
    # other NodeJS API running on port 3000 used by another website
            location /psychology/ {
                    proxy_pass http://localhost:3000;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header Access-Control-Allow-Origin *;
            }
    
    # other NodeJS API running on port 3001 used by another website
            location /novels/ {
                    proxy_pass http://localhost:3001;
                    proxy_set_header Host $host;
                    proxy_set_header X-Real-IP $remote_addr;
                    proxy_set_header Access-Control-Allow-Origin *;
            }
    }
    

    What can I try next?

    0

    There are 0 answers