Search code examples
node.jsflaskelectronflask-cors

Electron and Flask: Server-Side Session Variables Repeatedly Initializing


Short version: I'm trying to make an Electron app with Flask as the backend. Part of what I'm trying to do involves getting a session variable or initializing it if it doesn't exist yet. This does not work, as the session variable isn't recognized even when it is initialized, causing new variables to be created every time the route is called. I suspect this is a problem with my CORS configuration or lack thereof, but I don't know what's wrong or how to fix it. Thus this post.

Long version: My app takes the content of a form and passes it to the backend, which adds it as a node which is then appended to a linked list, which can then be accessed later. I want one of the fields of the node, detail, to be unique. And, to do so, I want to make it so that the user cannot submit a form where the value of "detail" already exists. I am attempting to achieve this with the following JS code:

/* INPUT VALIDATION */
async function checkUniqueDetail(detail_to_check){
    try{
        const response=await fetch(`${SERVER_URL}/check_detail/${detail_to_check}`,{
            method: "GET",
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
            },
        });
        const data=await response.json();
        const existing_details=data.detail_list;
        console.log("Existing details:", existing_details);
        console.log("Checking detail:", detail_to_check);
        const result=data.result;
        console.log("Result is:",result);
        if(result=="True"){
            return false;
        }else{
            console.log("The detail is unique");
            return true;
        }
    }catch(e){
        console.error(e);
        return false;
    }
}
/* FORM SUBMISSION */
function confirmFormSubmit(formData){
    if(confirm("Are you sure you want to submit?")){
        const qd=formData.get("detail");
        console.log("q_detail is:",qd);
        /* Form validation */
        checkUniqueDetail(qd).then(result=>{
            isUniqueDetail=result;
            console.log(qd,"unique?:",isUniqueDetail);
            /* If validated, send info */
            if(isUniqueDetail==true){
                sendForm();
                sendDetail(qd);
                message.innerText="Question added!"
            }else{
                message.innerText="A question with this detail already exists. Please try again"
            }
        });
    }
}
form.addEventListener("submit", async (event)=>{
    event.preventDefault();
                
    const fData = new FormData(form);
    confirmFormSubmit(fData);
});

This calls to the routes made by the Flask app that look like this:

# ROUTES
from flask import Flask, request, session
from flask_session import Session
from flask_cors import CORS
import redis, json
#for forcibly clearing session files
import atexit

app = Flask(__name__)

# Configurations
app.config["SECRET_KEY"]="change_later"
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_PERMANENT"] = False
r = redis.from_url('redis://127.0.0.1:6379')
app.config['SESSION_REDIS'] = r
def test_redis_connection(redis_session):
    """Check that Redis is connected to"""
    try:
        redis_session.ping()  # Check if Redis is alive
        print("Redis connection successful!")
    except redis.exceptions.ConnectionError as e:
        print(f"Redis connection error: {e}")
        exit()  # Or handle the error appropriately
test_redis_connection(r)
app.config["CORS_HEADERS"] = "Content-Type"

# Initialize Plugins
sess=Session()
sess.init_app(app)
CORS(app,resources={r"/*": {"origins": "http://localhost*"}},)

#check that the server's running and connected
@app.route("/", methods=["GET","POST"])
def check():
    return {"result":"Server active!"}

### ERROR!!!: This only works as a Flask app
#       It seems like every time this is run,
#       it makes a new session variable, rather than getting the old one
@app.route("/add_detail/<detail>",methods=["GET","POST"])
def add_detail_to_list(detail):
    """Add a q_detail to a list of q_details"""
    # Initialize the list if it doesn't exist
    if 'lst' not in session:
        print("Session variable not found. Initializing...")
        session['lst'] = json.dumps([])
        session.modified = True

    # Append to the list
    lst:list[str]=json.loads(session['lst'])
    print("Before appending:",lst)
    lst.append(detail)
    print("After appending:",lst)
    session['lst'] = json.dumps(lst)
    session.modified = True
    
    return {"response":f"{lst}"}
@app.route("/get_all_details",methods=["GET"])
def get_all_details():
    details:list[str]=session.get("lst",[])
    return {"result":details}
@app.route("/check_detail/<detail>")
def check_detail(detail):
    details:list[str]=session.get("lst",[])
    if detail in details:
        return {"result":"True","detail_list":details}
    else:
        return {"result":"False","detail_list":details}


## NOTE: Make sure this works in production
### ERROR!!! Doesn't work with Electron app
#       Maybe due to how the Flask app is killed?
def clear_redis_sessions(redis_session):
    """Clears all session data from Redis."""
    try:
        for key in redis_session.keys("session:*"): # Important: Use a pattern to only delete session keys
            redis_session.delete(key)
        print("Redis sessions cleared.")
    except Exception as e:
        print(f"Error clearing Redis sessions: {e}")
atexit.register(clear_redis_sessions,redis_session=r)  # Register the cleanup function


# print(app.url_map)

if __name__ == "__main__":
    app.run(debug=True)

It says it in the comments, but the error occurs with the "/add_detail/" route, specifically in the initial if statement. It triggers every time, creating a new session["lst"] variable rather than getting the old one. This means that the list the frontend gets is always empty, so the detail is always marked as unique and the form is always allowed to be sent. Here's a screenshot of the Electron app, showing this happening: Screenshot of Electron app showing the error

And a screenshot of my redis-cli showing the two session keys that were created: screenshot of my redis-cli showing the two session keys that were created

I suspect that the reason this is happening is due to the nature of cross-site requests. This is because this problem doesn't occur if I run the Flask app independently: Image of Flask app working independently Image of Redis-Cli showing session variable working correctly but does if I use curl: enter image description here enter image description here

I have CORS enabled, but I suspect I don't have it configured properly. But maybe it also has to do with how I'm connecting to the Flask app. Here's my main.js just in case:

const { app, BrowserWindow } = require('electron');
const { exec } = require("child_process");

/**
 * Connects to the Flask app
 */
const connectToFlask=function(){
    let python;
    //test version
    python = require('child_process').spawn('py', ['./py/test_routes.py']);
    //executable version
    //python = require('child_process').execFile("routes.exe");
    python.stdout.on('data', function (data) {  
        console.log("FLASK RUNNING! data:", data.toString('utf8'));  
    });  
    python.stderr.on('data', (data) => {    // when error
        console.error(`stderr: ${data}`);
        console.log(`stderr: ${data}`);
    });
    python.on("close", (code)=>{
        console.log(`child process exited with code ${code}`);
    });
}

/**
 * Create a new BrowserWindow that's connected to Flask with index.html as its UI
 */
const createWindow = () => {
    const win = new BrowserWindow({
        width: 800,
        height: 600
    });

    connectToFlask();

    win.loadFile('index.html');
}

/**
 * When all the windows close, quit the app
 */
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin'){
        /* Kill the Flask app on close too */
        exec("taskkill /f /t /im python.exe", (err, stdout, stderr) => {
            if (err) {
                console.error(err);
                return;
            }
            console.log(`stdout: ${stdout}`);
            console.log(`stderr: ${stderr}`);
            console.log('Flask process terminated');
        });
        app.quit();
    }
});

/**
 * Wait for the app to be ready, then create a new window
 */
app.whenReady().then(() => {
    createWindow();
});

Does anyone know what I'm doing wrong and how to fix it?

Thank you in advance


Solution

  • Okay, I found the solution. Not only did I need to allow requests with cookies from both sides like Detlef said, but I also needed to add:

    app.config["SESSION_COOKIE_SAMESITE"]="None"
    app.config["SESSION_COOKIE_SECURE"]=True
    

    to the configurations.

    So the full Flask app looks like:

    # ROUTES
    from flask import Flask, request, session
    from flask_session import Session
    from flask_cors import CORS, cross_origin
    import redis, json
    
    app = Flask(__name__)
    
    # Configurations
    app.config["SECRET_KEY"]="change_later"
    app.config["SESSION_TYPE"] = "redis"
    app.config["SESSION_PERMANENT"] = False
    app.config["SESSION_COOKIE_SAMESITE"]="None"
    app.config["SESSION_COOKIE_SECURE"]=True
    r = redis.from_url('redis://127.0.0.1:6379')
    app.config['SESSION_REDIS'] = r
    def test_redis_connection(redis_session):
        """Check that Redis is connected to"""
        try:
            redis_session.ping()  # Check if Redis is alive
            print("Redis connection successful!")
        except redis.exceptions.ConnectionError as e:
            print(f"Redis connection error: {e}")
            exit()  # Or handle the error appropriately
    test_redis_connection(r)
    app.config["CORS_HEADERS"] = "Content-Type"
    
    # Initialize Plugins
    sess=Session()
    sess.init_app(app)
    CORS(app,supports_credentials=True)
    
    #check that the server's running and connected
    @app.route("/", methods=["GET","POST"])
    def check():
        return {"result":"Server active!"}
    
    @app.route("/add_detail/<detail>",methods=["GET","POST"])
    def add_detail_to_list(detail):
        """Add a q_detail to a list of q_details"""
        # Initialize the list if it doesn't exist
        if 'lst' not in session:
            print("Session variable not found. Initializing...")
            session['lst'] = json.dumps([])
            session.modified = True
    
        # Append to the list
        lst:list[str]=json.loads(session['lst'])
        print("Before appending:",lst)
        lst.append(detail)
        print("After appending:",lst)
        session['lst'] = json.dumps(lst)
        session.modified = True
        
        return {"response":f"{lst}"}
    @app.route("/get_all_details",methods=["GET"])
    def get_all_details():
        details:list[str]=session.get("lst",[])
        return {"result":details}
    @app.route("/check_detail/<detail>")
    def check_detail(detail):
        details:list[str]=session.get("lst",[])
        if detail in details:
            return {"result":"True","detail_list":details}
        else:
            return {"result":"False","detail_list":details}
    
    # print(app.url_map)
    
    if __name__ == "__main__":
        app.run(debug=True)
    

    And the form looks like:

    <!DOCTYPE html>
    <html lang='en'>
        <head>
            <meta charset='UTF-8' />
            <meta name='viewport' content='width=device-width, initial-scale=1.0' />
            <title>Test Form</title>
            <style>
                label{
                    display: block;
                }
            </style>
        </head>
        <body>
            <h1>Test Form</h1>
            <p id="message"></p>
            <form id="form">
                <label>* Question:<input type="text" name="question" id="question" placeholder="Please enter the JOB NAME" required /></label>
                <label>* Detail:<input type="text" name="detail" id="detail" placeholder="Job Name" required /></label>
                <button type="submit" id="submit">Continue</button>
            </form>
    
            <script>
                const SERVER_URL = "http://127.0.0.1:5000";
                const form=document.getElementById("form");
                const message=document.getElementById("message");
                
                /* INPUT VALIDATION */
                async function checkUniqueDetail(detail_to_check){
                    try{
                        const response=await fetch(`${SERVER_URL}/check_detail/${detail_to_check}`,{
                            method: "GET",
                            credentials:"include",
                            headers: {
                                Accept: "application/json",
                                "Content-Type": "application/json",
                            },
                        });
                        const data=await response.json();
                        const existing_details=data.detail_list;
                        console.log("Existing details:", existing_details);
                        console.log("Checking detail:", detail_to_check);
                        const result=data.result;
                        console.log("Result is:",result);
                        if(result=="True"){
                            return false;
                        }else{
                            console.log("The detail is unique");
                            return true;
                        }
                    }catch(e){
                        console.error(e);
                        return false;
                    }
                }
                /* FORM SUBMISSION */
                async function sendForm() {
                    console.log("Reached node creation");
                }
                async function sendDetail(detail) {
                    try{
                        const response=await fetch(`${SERVER_URL}/add_detail/${detail}`,{
                            method: "POST",
                            credentials:"include",
                            headers: {
                                Accept: "application/json",
                                "Content-Type": "application/json",
                            }
                        });
                        const data=await response.json();
                        const details=data.response;
                        console.log("Added detail. Details are now:", details);
                    }catch(e){
                        console.error(e);
                    }
                }
                /* 
                    Confirm the user wants to submit the form. If so, run form validation and submission
                */
                function confirmFormSubmit(formData){
                    if(confirm("Are you sure you want to submit?")){
                        const qd=formData.get("detail");
                        console.log("q_detail is:",qd);
                        /* Form validation */
                        checkUniqueDetail(qd).then(result=>{
                            isUniqueDetail=result;
                            console.log(qd,"unique?:",isUniqueDetail);
                            /* If validated, send info */
                            if(isUniqueDetail==true){
                                sendForm();
                                sendDetail(qd);
                                message.innerText="Question added!"
                            }else{
                                message.innerText="A question with this detail already exists. Please try again"
                            }
                        });
                    }
                }
                form.addEventListener("submit", async (event)=>{
                    event.preventDefault();
                    
                    const fData = new FormData(form);
                    confirmFormSubmit(fData);
                });
    
            </script>
        </body>
    </html>
    

    Thanks everyone for your help!