Skip to content

Commit 84c2d09

Browse files
committed
feat(frontend): api interaction layer
1 parent 716e59c commit 84c2d09

File tree

2 files changed

+151
-0
lines changed

2 files changed

+151
-0
lines changed
+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
(ns parts.frontend.utils.api
2+
(:require
3+
[cljs.core.async :refer [chan put! <!]]
4+
[cljs.core.async.interop :refer-macros [<p!]])
5+
(:require-macros
6+
[cljs.core.async.macros :refer [go]]))
7+
8+
(def ^:private token-storage-key "parts-auth-tokens")
9+
(def ^:private user-email-key "parts-user-email")
10+
(def csrf-token-name "__anti-forgery-token")
11+
12+
(defn save-tokens
13+
"Save authentication tokens to local storage"
14+
[tokens]
15+
(.setItem js/localStorage token-storage-key (js/JSON.stringify (clj->js tokens))))
16+
17+
(defn get-tokens
18+
"Get authentication tokens from local storage"
19+
[]
20+
(when-let [tokens-str (.getItem js/localStorage token-storage-key)]
21+
(js->clj (.parse js/JSON tokens-str) :keywordize-keys true)))
22+
23+
(defn save-user-email
24+
"Save user email to local storage"
25+
[email]
26+
(.setItem js/localStorage user-email-key email))
27+
28+
(defn get-user-email
29+
"Get user email from local storage"
30+
[]
31+
(.getItem js/localStorage user-email-key))
32+
33+
(defn clear-tokens
34+
"Clear authentication tokens from local storage"
35+
[]
36+
(.removeItem js/localStorage token-storage-key)
37+
(.removeItem js/localStorage user-email-key))
38+
39+
(defn get-auth-header
40+
"Get the Authorization header for authenticated requests"
41+
[]
42+
(when-let [tokens (get-tokens)]
43+
(str (:token_type tokens) " " (:access_token tokens))))
44+
45+
(defn get-csrf-token
46+
"Get the CSRF token from the meta tag"
47+
[]
48+
(when-let [meta-tag (.querySelector js/document "meta[name='csrf-token']")]
49+
(.getAttribute meta-tag "content")))
50+
51+
(defn api-request
52+
"Make a request to the API, optionally with authentication"
53+
[{:keys [url method data authenticated? as-form?]
54+
:or {method "GET"
55+
authenticated? false
56+
as-form? false}}]
57+
(let [result-chan (chan)
58+
csrf-token (get-csrf-token)
59+
headers (js/Object.)
60+
_ (when (not as-form?)
61+
(aset headers "Content-Type" "application/json"))
62+
_ (when authenticated?
63+
(when-let [auth-header (get-auth-header)]
64+
(aset headers "Authorization" auth-header)))
65+
_ (js/console.log "API request:", url, method, as-form?, "data:", data)]
66+
(go
67+
(try
68+
(let [request-data (if data
69+
data
70+
{})
71+
fetch-opts (if as-form?
72+
;; For form data submission
73+
(let [form-data (js/URLSearchParams.)]
74+
;; Add all data as form fields
75+
(doseq [[k v] request-data]
76+
(when v
77+
(js/console.log "Adding to form data:" (name k) v)
78+
(.append form-data (name k) v)))
79+
80+
;; Set proper content type for form submissions
81+
(aset headers "Content-Type" "application/x-www-form-urlencoded")
82+
83+
;; Create fetch options
84+
(clj->js {:method method
85+
:headers headers
86+
:body form-data}))
87+
;; For JSON, use the standard approach
88+
(clj->js {:method method
89+
:headers headers
90+
:body (js/JSON.stringify (clj->js request-data))}))
91+
response (<p! (js/fetch url fetch-opts))
92+
status (.-status response)
93+
content-type (.get (.-headers response) "content-type")
94+
_ (js/console.log "Response content type:", content-type)
95+
is-json (when content-type (re-find #"application/json" content-type))
96+
body (<p! (if is-json
97+
(.json response)
98+
(.text response)))
99+
parsed-body (if is-json (js->clj body :keywordize-keys true) body)]
100+
(js/console.log "API Response:", status, parsed-body)
101+
(if (< status 400)
102+
(do
103+
(js/console.log "API Success:", parsed-body)
104+
(put! result-chan {:success true :data parsed-body}))
105+
(do
106+
(js/console.log "API Error:", status, parsed-body)
107+
(put! result-chan {:success false :error parsed-body}))))
108+
(catch js/Error e
109+
(put! result-chan {:success false :error (.-message e)}))))
110+
result-chan))
111+
112+
(defn login
113+
"Attempt to log in with email and password"
114+
[email password csrf-token]
115+
(js/console.log "Attempting login with:" email "token:" csrf-token)
116+
(api-request
117+
{:url "/api/auth/login"
118+
:method "POST"
119+
:as-form? true
120+
:data {"email" email
121+
"password" password
122+
"__anti-forgery-token" csrf-token}}))
123+
124+
(defn logout
125+
"Log out the current user"
126+
[]
127+
(let [tokens (get-tokens)
128+
csrf-token (get-csrf-token)
129+
result-chan (chan)]
130+
(if tokens
131+
(go
132+
(let [response (<! (api-request
133+
{:url "/api/auth/logout"
134+
:method "POST"
135+
:authenticated? true
136+
:as-form? true
137+
:data {"refresh_token" (:refresh_token tokens)
138+
"__anti-forgery-token" csrf-token}}))]
139+
(clear-tokens)
140+
(put! result-chan {:success true})))
141+
(put! result-chan {:success true}))
142+
result-chan))
143+
144+
(defn get-user-info
145+
"Get the current user's information"
146+
[]
147+
(api-request
148+
{:url "/api/account"
149+
:method "GET"
150+
:authenticated? true}))

src/main/parts/views/partials.clj

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
2626
[:meta {:name "description" :content description}]
2727
[:meta {:name "theme-color" :content "#62a294"}]
28+
[:meta {:name "csrf-token" :content *anti-forgery-token*}]
2829
[:link {:rel "icon" :sizes "192x192" :href "/images/icons/favicon.png"}]
2930
[:link {:rel "apple-touch-icon" :href "/images/icons/favicon.png"}]
3031
[:title (if title

0 commit comments

Comments
 (0)