I'm running into a problem with getting my Clojure web server working.

This is the frontend:

(ns ^:figwheel-hooks simple-server.core
  (:require [clojure.string :as str]
            [ajax.core :refer [GET POST]]
            [goog.dom :as gdom]
            [goog.events :as events]
            [reagent.core :as r :refer [atom]]
            [reagent.dom :as rdom]
            [reitit.core :as reitit]))
(def backend-url "http://localhost:3000")

(def guess-val      (r/atom 5))

(def guess-text "Guess the number I'm thinking of!")
(defonce app-state (r/atom {:text guess-text}))

(defn slider-on-change-handler [js-event]
  (reset! guess-val (-> js-event .-target .-value)))

(defn multiply [a b] (* a b))

(defn form-sanitizer [input]
  (str/lower-case (str/trim input)))


(defn get-app-element []
  (gdom/getElement "app"))


;; Frontend event handler to update username and password
(defn update-credentials [field value]
  (swap! app-state assoc field value))


;; Frontend event handler to submit the login form
(defn submit-login []
  (let [{:keys [username password]} @app-state]
    (POST (str backend-url "/api/login")
          {:params {:username (form-sanitizer username) :password password}
           :handler (fn [response]
                      (swap! app-state merge
                             {:user-authenticated true
                              :message "Login successful, start guessing!"}))
           :error-handler (fn []
                            (swap! app-state assoc :message "Login failed! Try again."))})))

;; Frontend event handler to submit guesses to the backend
(defn submit-guess []
  (let [guess (@app-state :guess)]
    (POST (str backend-url "/api/guess")
          {:params {:guess guess}
           :handler (fn [response]
                      (swap! app-state assoc :message (str "Server says: " response)))
           :error-handler (fn []
                            (swap! app-state assoc :message "Error submitting guess."))})))

;; Login component
(defn login-form [] 
    [:div.login-form
               [:h1 "Login"]
               [:input {:type "text"
                        :placeholder "Username"
                        :on-change #(update-credentials :username (-> % .-target .-value))}]
               [:input {:type "password"
                        :placeholder "Password"
                        :on-change #(update-credentials :password (-> % .-target .-value))}]
               [:button {:on-click submit-login} "Login"]
               [:div.message (@app-state :message)]])




(defn guessing-game []
  [:div.guessing-game
   [:h3 "Guess the number I'm thinking of!"]
   [:input {:type "number"
            :value (@app-state :guess)
            :on-change #(swap! app-state assoc :guess (-> % .-target .-value))}]
   [:button {:on-click submit-guess} "Submit Guess"]
   [:div.message (@app-state :message)]])


(defn guess-page []
  [:div
   [:h1 "Guessing Game"]
   [:h3 (:text @app-state)]
   [:div {:class "slidecontainer"}
    [:input {:type  "range"
             :id    "MyRange1"
             :min   1
             :max   10
             :value 5
             :on-change slider-on-change-handler}]]
   [:h3 @guess-val]])

;; Conditional rendering based on authentication
(defn app-root []
  (if (@app-state :user-authenticated)
    [guessing-game]
    [login-form]))

(defn mount-app []
  (rdom/render [app-root] (.getElementById js/document "app")))

(defn ^:after-load on-reload []
  ;; Re-mount the app after code reloading occurs
  (mount-app))

;; Initial mounting of the app
(mount-app)


This is the backend:

(ns simple-server.server
  (:require
   [clojure.pprint]
   [clojure.string :as str]
   [compojure.coercions   :refer [as-int]]
   [compojure.core        :refer [ANY defroutes GET POST OPTIONS]]
   [compojure.route           :as route]
   [ring.adapter.jetty    :refer [run-jetty]]
   [ring.middleware.defaults  :as middleware]
   [ring.middleware.cookies   :as cookies]
   [ring.middleware.multipart-params :as multi]
   [ring.middleware.cors :refer [wrap-cors]]
   [ring.mock.request         :as mock]
   [ring.util.response        :as ring
    :refer [not-found redirect response status]]
   [simple-server.simple-game :as game]
   [simple-server.db          :as db]
   [byte-transforms           :as transforms]))

;; (defn random-api []
;;   (println "I've been called!")
;;   {:status 200
;;    :body (pr-str {:lucky-number (rand-int 1000)
;;                   :a-set #{1 "foo" :baz [::a ::b]}})
;;    :headers {"Content-Type" "application/edn"}})

(defn set-session-cookie [response user-id]
  (let [cookie-value (transforms/hash (str user-id) :crc32)] ; Generate a simple cookie value based on the user ID
    (ring/set-cookie response "token" cookie-value {:max-age 3600}))) ; Set cookie with a maximum age of 1 hour

(defn get-session-cookie [request]
  (get-in request [:cookies "token" :value]))


(defn new-game-handler [request]
  (when (game/new-game! (get-session-cookie request))
    (response (str "OK - start guessing at /guess/?guess=N"))))


(defn valid-login? [token password] ;; placeholder login logic...
  (or (and (= token "foo") (= password "bar"))
      (and (= token "admin") (= password "123"))
      (and (= token "user") (= password "pass"))
      (and (= token "egg") (= password "man"))
      (and (= token "test") (= password "test"))))

;; ;; tried to combine these functions but the login page is very fragile
;; (defn login-page-handler []
;;   (response (slurp "resources/public/login.html")))
;; ;; (defn guess-page-handler []
;; ;;   (response (slurp "res/guess.html")))


(defn validate-user-credentials [username password]
  (let [user-id-map (first (db/get-user-id username)) 
        user-id (:user_id user-id-map)                 
        password-map (first (db/get-password user-id))
        stored-password (:password password-map)]
    (= password stored-password)))



(defn login-api-handler [request]
  (let [params (get request :params)
        username (:username params)
        password (:password params)]
    (if (validate-user-credentials username password)
      (let [user-id (-> (first (db/get-user-id username))
                        :user_id)]
        (-> (response {:status "success", :message "Login successful"})
            (set-session-cookie user-id)
            (ring/content-type "application/json")))
      (ring/response {:status "error", :message "Invalid login. Try again."}))))

(defn guess-api-handler [request]
  (let [params (get request :params)
        guess (Integer/parseInt (:guess params))
        token (get-session-cookie request)]
    (if (nil? token)
      (ring/response {:status "error", :message "Unauthorized"})
      (let [result (game/guess-answer guess token)]
        (response {:status "success", :message (name result), :tries-left (db/get-remaining-tries token)})))))

(defn guess-handler [guess user-hash]
  (condp = (game/guess-answer guess user-hash)
    nil       (-> (response  "You need to supply a guess with /guess?guess=N")
                  (status 400))
    :game-win  (response  (str "Congratulations! You win!"))
    :game-over (response  "Too bad! You ran out of tries!")
    :too-low   (response  (str "Too low! "  (db/get-remaining-tries user-hash) " tries remaining!"))
    :too-high  (response  (str "Too high! " (db/get-remaining-tries user-hash) " tries remaining!"))))


(defroutes site-routes

  ;; (GET  "/login"             []                          (login-page-handler))
  ;; (POST "/login"             request                     (login-handler request))
  (POST "/api/login"         request                     (login-api-handler request))
  (POST "/api/guess"         request                     (guess-api-handler request))
  (OPTIONS "*"               []                          {}) ;; handle preflight requests
  
  (GET  "/new-game"          request                     (new-game-handler request))
  ;; (GET  "/guess:token"    [token]                     (guess-page-handler token))
  (GET  "/guess"             [guess :<< as-int :as request]  (guess-handler guess (get-session-cookie request)))
  (ANY  "*"                  []                          (not-found (str "Sorry, no such URI on this server!\n\n
                                                                         Navigate to /new-game to start the guessing game.\n
                                                                         If you're in the middle of a game, go to /guess/?guess=N instead."))))

(defn add-content-type-htmltext-header [handler]
  (fn [request]
    (let [response (handler request)]
      (-> response
          (ring/header "Content-Type" "text/html")))))


(defn redirect-to-login-middleware
  "If a login cookie (at present, can be anything) is not found, redirects user to the login page."
  [handler]
  (fn [request]
    (let [token (get-session-cookie request)]
      (clojure.pprint/pprint request)
      (if (nil? token)
        (-> (handler (assoc request :uri "/login"))  ; Redirect to /login
            (ring/header "Content-Type" "text/html"))
        (handler request)))))


(def handler
  (-> site-routes
      (wrap-cors :access-control-allow-origin [#".localhost:9500"] ;; replace with your frontend domain/URL
                 :access-control-allow-credentials true
                 :access-control-allow-methods [:get :post :put :delete :options]
                 :access-control-allow-headers ["Content-Type" "Authorization"]
                 :access-control-max-age 3600)
      (middleware/wrap-defaults middleware/api-defaults)
      (redirect-to-login-middleware)
      (add-content-type-htmltext-header)
      (multi/wrap-multipart-params)
      (cookies/wrap-cookies)))

(comment
  (handler (mock/request :get "/new-game"))
  (handler (mock/request :get "/guess?guess=3"))
  (handler (mock/request :get "/dunno")))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defonce server
  (future (run-jetty #'handler {:port 3000 :join? false})))


This is the error when I try to send a login request:

Access to XMLHttpRequest at 'http://localhost:3000/api/login' from origin 'http://localhost:9500' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
xhrio.js:207 
        
        
       POST http://localhost:3000/api/login net::ERR_FAILED