1 (uiop:define-package
#:lw2.login
2 (:use
#:cl
#:lw2-viewer.config
#:lw2.utils
#:lw2.graphql
#:lw2.backend
#:lw2.backend-modules
#:alexandria
#:cl-json
#:flexi-streams
#:websocket-driver-client
)
3 (:import-from
#:ironclad
#:byte-array-to-hex-string
#:digest-sequence
)
4 (:import-from
#:lw2.context
#:*current-backend
*)
5 (:export
#:do-lw2-resume
#:do-login
#:do-lw2-create-user
#:do-lw2-forgot-password
#:do-lw2-reset-password
#:do-logout
6 #:do-login-with-oidc-access-token
7 #:do-lw2-post-query
#:do-lw2-post-query
*
8 #:do-lw2-post
#:do-lw2-post-edit
#:do-lw2-post-remove
#:do-lw2-comment
#:do-lw2-comment-edit
#:do-lw2-comment-remove
9 #:do-lw2-vote
#:do-user-edit
#:do-create-conversation
#:do-create-message
)
10 (:unintern
#:parse-login-result
)
11 (:recycle
#:lw2.utils
))
13 (in-package #:lw2.login
)
15 (defparameter *sockjs-debug-output
* nil
)
17 (declaim (inline maybe-output
))
18 (defun maybe-output (stream prefix message
)
19 (if stream
(format stream
"~&~A: ~A~%" prefix message
))
22 (defun sockjs-encode-alist (alist)
23 (encode-json-to-string (list (encode-json-alist-to-string alist
))))
25 (defun sockjs-decode (msg)
26 (if (eq (elt msg
0) #\a)
27 (let ((response (map 'list
#'decode-json-from-string
(decode-json-from-string (subseq msg
1)))))
28 (if (= (length response
) 1)
30 (error "Unsupported sockjs message.")))))
32 (defun password-digest (password &key
(algorithm :sha256
))
33 (byte-array-to-hex-string
34 (digest-sequence algorithm
35 (string-to-octets password
:external-format
:utf8
))))
37 (defun do-lw2-sockjs-operation (operation)
38 (let ((client (wsd:make-client
(concatenate 'string
(websocket-uri *current-backend
*) "sockjs/329/" (random-string 8) "/websocket")
39 :additional-headers
(forwarded-header)))
40 (debug-output *sockjs-debug-output
*)
41 (result-semaphore (sb-thread:make-semaphore
))
45 (wsd:start-connection client
)
46 (wsd:on
:message client
(lambda (encoded-message)
47 (maybe-output debug-output
"sockjs recd" encoded-message
)
48 (let ((message (sockjs-decode encoded-message
)))
49 (switch ((cdr (assoc :msg message
)) :test
'equal
)
51 (wsd:send client
(maybe-output debug-output
"sockjs sent" (sockjs-encode-alist operation
))))
54 (sb-thread:signal-semaphore result-semaphore
))))))
55 (wsd:send client
(maybe-output debug-output
"sockjs sent" (sockjs-encode-alist (alist "msg" "connect"
58 (unless (sb-thread:wait-on-semaphore result-semaphore
:timeout
10)
59 (error "Timeout while waiting for LW2 server.")))
60 (wsd:close-connection client
))
63 (defun do-lw2-sockjs-method (method &rest params
)
64 (do-lw2-sockjs-operation (alist :msg
"method"
69 (defun parse-websocket-login-result (result)
70 (let* ((result-inner (cdr (assoc :result result
)))
71 (userid (cdr (assoc :id result-inner
)))
72 (token (cdr (assoc :token result-inner
)))
73 (expires (cdadr (assoc :token-expires result-inner
))))
74 (if (and userid token
)
75 (values userid token nil
(and expires
(floor expires
1000)))
76 (if-let (error-message (cdr (assoc :reason
(cdr (assoc :error result
)))))
77 (values nil nil error-message
)
78 (error "Unknown response from LW2: ~A" result
)))))
80 (defun do-graphql-post-query (auth-token data
)
81 (call-with-http-response
83 (graphql-uri *current-backend
*)
86 :headers
(backend-request-headers auth-token t
)
87 :content
(encode-json-to-string data
)))
89 (defun do-lw2-resume (auth-token)
90 (let ((result (do-lw2-sockjs-method "login" (alist :resume auth-token
))))
91 (parse-websocket-login-result result
)))
93 (define-backend-function do-login
(user-designator password
&key
(try-legacy t
))
94 (backend-websocket-login
95 (let ((result (do-lw2-sockjs-method "login"
96 (alist :user
(alist "username" user-designator
)
97 :password
(alist :digest
(password-digest password
)
98 :algorithm
"sha-256")))))
100 ((assoc :error
(trivia:alist
(:error .
"legacy-account")
101 (:details .
(trivia:alist
(:salt . legacy-salt
)
102 (:username . legacy-username
)))))
104 (do-login user-designator
105 (format nil
"~A~A" legacy-salt
106 (password-digest (format nil
"~A~A ~A" legacy-salt legacy-username password
)
109 (values nil nil
"Incorrect password")))
111 (parse-websocket-login-result result
))))))
113 (defun do-login-with-oidc-access-token (access-token)
114 (multiple-value-bind (body status response-headers
)
115 (dex:get
(quri:make-uri
:defaults
(quri:uri
(graphql-uri *current-backend
*))
116 :path
"/auth/useAccessToken"
117 :query
(alist "access_token" access-token
))
120 (unless (< status
400)
121 (error "Login failed: ~A" body
))
122 (let ((cookies (gethash "set-cookie" response-headers
)))
123 (or (some (lambda (cookie)
125 ("\\bloginToken=([^;]+)"
128 (error "Backend server did not send login token.")))))
130 (define-backend-function do-lw2-create-user
(username email password
)
131 (backend-websocket-login
132 (let ((result (do-lw2-sockjs-method "createUser"
133 (alist :username username
135 :password
(alist :digest
(password-digest password
)
136 :algorithm
"sha-256")))))
137 (parse-websocket-login-result result
))))
139 (define-backend-function do-lw2-forgot-password
(email)
140 (backend-websocket-login
141 (let ((result (do-lw2-sockjs-method "forgotPassword"
142 (alist :email email
))))
143 (if-let (error-data (cdr (assoc :error result
)))
144 (values nil
(cdr (assoc :reason error-data
)))
147 (define-backend-function do-lw2-reset-password
(auth-token password
)
148 (backend-websocket-login
149 (let ((result (do-lw2-sockjs-method "resetPassword"
151 (alist :digest
(password-digest password
)
152 :algorithm
"sha-256"))))
153 (parse-websocket-login-result result
))))
155 (define-backend-function do-logout
(auth-token)
156 (backend-websocket-login
157 (declare (ignore auth-token
))))
159 (defun parse-passport-js-login-result (result)
160 (let* ((res-errors (first (cdr (assoc :errors result
))))
161 (res-data (cdr (first (cdr (assoc :data result
)))))
162 (token (cdr (assoc :token res-data
))))
164 (values nil nil
(cdr (assoc :message res-errors
)))
165 (let ((user-id (cdr (first (do-lw2-post-query
167 (graphql-query-string :current-user nil
'(:--id
))))))))
168 (values user-id token nil
)))))
170 (defun do-passport-js-login-operation (operation params
)
171 (parse-passport-js-login-result
172 (do-graphql-post-query nil
(alist :query
(graphql-operation-string :mutation operation params
'(:token
))))))
174 (define-backend-operation do-login backend-passport-js-login
(user-designator password
&key try-legacy
)
175 (declare (ignore try-legacy
))
176 (do-passport-js-login-operation :login
(alist :username user-designator
177 :password password
)))
179 (define-backend-operation do-lw2-create-user backend-passport-js-login
(username email password
)
180 (do-passport-js-login-operation :signup
(alist :username username
182 :password password
)))
184 (define-backend-operation do-logout backend-passport-js-login
(auth-token)
185 (do-graphql-post-query auth-token
(alist :query
(graphql-operation-string :mutation
:logout nil
'(:token
)))))
187 ; (do-lw2-post-query "OCP7NeJEW9fPpYGG_nCN3g0felGTTNd0eg5uiLNQqBR" `((("query" . "mutation vote($documentId: String, $voteType: String, $collectionName: String) { vote(documentId: $documentId, voteType: $voteType, collectionName: $collectionName) { ... on Post { currentUserVotes { _id, voteType, power } } } }") ("variables" ("documentId" . "sqhAntEGpYgFXXH2H") ("voteType" . "upvote") ("collectionName" . "Posts")) ("operationName" . "vote"))))
189 (defun do-lw2-post-query (auth-token data
)
190 (lw2.backend
::do-graphql-debug data
)
191 (let* ((response-alist (do-graphql-post-query auth-token data
))
192 (res-errors (cdr (assoc :errors response-alist
)))
193 (res-data (rest (first (cdr (assoc :data response-alist
))))))
195 (res-errors (lw2.backend
:signal-lw2-errors res-errors
))
197 (t (error "Unknown response from LW2 server: ~A" response-alist
)))))
199 (defun do-lw2-post-query* (auth-token data
)
200 (cdr (assoc :--id
(do-lw2-post-query auth-token data
))))
202 (define-backend-function lw2-mutation-string
(target-type mutation-type terms fields
)
204 (let* ((mutation-type-string (case mutation-type
208 (mutation-name (concatenate 'string
209 (if (eq target-type
:user
)
210 (string-downcase target-type
)
211 (string-capitalize target-type
))
212 "s" mutation-type-string
)))
213 (values (graphql-mutation-string mutation-name terms fields
) mutation-name
)))
214 (backend-lw2-modernized
215 (let* ((mutation-name (concatenate 'string
(string-downcase mutation-type
) (string-capitalize target-type
)))
216 (selector-type (concatenate 'string
(string-capitalize target-type
) "SelectorUniqueInput"))
217 (data-type (concatenate 'string
(string-capitalize mutation-type
) (string-capitalize target-type
) "DataInput"))
219 (cdr (assoc :document terms
))
220 (cdr (assoc :set terms
))
221 (map 'list
(lambda (x) (cons (car x
) :null
)) (cdr (assoc :unset terms
)))))
222 (data (map 'list
(lambda (x) (destructuring-bind (k . v
) x
225 (alist :original-contents
(alist :data v
:type
"markdown")
231 (loop for
(k . v
) in terms nconc
236 (:document-id
(alist :selector
(alist :document-id v
)))
237 (t (list (cons k v
)))))
239 (nalist :data data
)))))
240 (values (with-output-to-string (stream)
241 (format stream
"mutation ~A(~@[$selector: ~A!, ~]$data: ~A!)~3:*{~A(~:[~;selector: $selector, ~]data: $data)"
243 (if (cdr (assoc :selector terms
)) selector-type
)
245 (write-graphql-simple-field-list (list (list* :data fields
)) stream
)
246 (write-string "}" stream
))
252 Low level graphQL mutation string builder. This is a good function to override
253 with a define-backend-operation and translate into the semantics of higher level functions
,
254 that way you can avoid having to recreate them all for each backend service.
256 auth-token - The authentication token to use with the API
, fairly straightforward.
257 target-type - The type of object that we
're mutating
258 mutation-type - The request method
(or analogous equivalent
) that we
're using
259 terms - The additional parameters
/variables
/etc that we
're sending to the server with our request
260 fields - The return values we want to get from the server after it completes our request
262 (define-backend-function do-lw2-mutation
(auth-token target-type mutation-type terms fields
)
264 (multiple-value-bind (mutation-string operation-name variables
)
265 (lw2-mutation-string target-type mutation-type terms fields
)
266 (do-lw2-post-query auth-token
(alist "query" mutation-string
267 "variables" variables
268 "operationName" operation-name
))))
269 (backend-lw2-modernized
270 (cdr (assoc :data
(call-next-method)))))
272 (defun do-lw2-post (auth-token data
)
273 (do-lw2-mutation auth-token
:post
:create
(alist :document data
) '(:--id
:slug
:html-body
)))
275 (defun do-lw2-post-edit (auth-token post-id set
&optional unset
)
276 (let* ((terms (alist* :document-id post-id
:set set
277 (alist-without-null :unset unset
))))
278 (declare (dynamic-extent terms
))
279 (do-lw2-mutation auth-token
:post
:update terms
'(:--id
:slug
:html-body
))))
281 (defun do-lw2-post-remove (auth-token post-id
)
282 (do-lw2-mutation auth-token
:post
:delete
(alist :document-id post-id
) '(:----typename
)))
284 (defun do-lw2-comment (auth-token data
)
285 (do-lw2-mutation auth-token
:comment
:create
(alist :document data
) '(:--id
:html-body
)))
287 (defun do-lw2-comment-edit (auth-token comment-id set
)
288 (do-lw2-mutation auth-token
:comment
:update
(alist :document-id comment-id
:set set
) '(:--id
:html-body
)))
290 (define-backend-function do-lw2-comment-remove
(auth-token comment-id
&key reason
)
292 (declare (ignore reason
)) ; reasons not supported
293 (do-lw2-mutation auth-token
:comment
:delete
(alist :document-id comment-id
) '(----typename)))
294 (backend-lw2-modernized
295 (do-lw2-comment-edit auth-token comment-id
(alist :deleted t
:deleted-public t
296 :deleted-reason reason
))))
298 (defun do-lw2-vote (auth-token target-collection target-id vote
)
299 (let* ((mutation (format nil
"setVote~:(~A~)" target-collection
))
300 (karma-vote (or (nonempty-string vote
)
301 (cdr (assoc :karma vote
))))
302 (extended-vote (remove :karma vote
:key
#'car
))
303 (ret (do-lw2-post-query auth-token
304 (alist "query" (graphql-mutation-string mutation
306 (alist :document-id target-id
307 :vote-type karma-vote
308 :extended-vote extended-vote
)
310 '(:--id
:base-score
:af
:af-base-score
:vote-count
:extended-score
311 :current-user-vote
:current-user-extended-vote
))
313 "operationName" mutation
)))
314 (confirmed-vote (block nil
315 (alist-bind (current-user-vote current-user-extended-vote
) ret
316 (return (list-cond* (current-user-vote :karma current-user-vote
)
317 current-user-extended-vote
))))))
318 (values confirmed-vote ret
)))
321 (defun do-user-edit (auth-token user-id data
)
322 (do-lw2-mutation auth-token
:user
:update
(alist :document-id user-id
:set data
) '(--id)))
324 (define-backend-function do-create-conversation
(auth-token data
)
326 (cdr (assoc :--id
(do-lw2-mutation auth-token
:conversation
:create
(alist :document data
) '(:--id
))))))
328 (define-backend-function generate-message-document
(conversation-id text
)
331 (alist :blocks
(loop for para in
(ppcre:split
"\\n+" text
)
332 collect
(alist :text para
:type
"unstyled"))
333 :entity-map
(make-hash-table))
334 :conversation-id conversation-id
))
335 (backend-lw2-modernized
337 :conversation-id conversation-id
)))
339 (define-backend-function do-create-message
(auth-token conversation-id text
)
341 (do-lw2-mutation auth-token
:message
:create
(alist :document
(generate-message-document conversation-id text
)) '(:--id
))))