mirror of
https://github.com/penpot/penpot.git
synced 2025-08-07 14:38:33 +02:00
feat(backend): rename uxbox.api to uxbox.http
This commit is contained in:
parent
3ff0ecee5f
commit
9d58d0fac5
19 changed files with 368 additions and 166 deletions
75
backend/src/uxbox/http/errors.clj
Normal file
75
backend/src/uxbox/http/errors.clj
Normal file
|
@ -0,0 +1,75 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 20162019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.http.errors
|
||||
"A errors handling for the http server.")
|
||||
|
||||
(defmulti handle-exception #(:type (ex-data %)))
|
||||
|
||||
(defmethod handle-exception :validation
|
||||
[err]
|
||||
(let [response (ex-data err)]
|
||||
{:status 400
|
||||
:body response}))
|
||||
|
||||
(defmethod handle-exception :default
|
||||
[err]
|
||||
(let [response (ex-data err)]
|
||||
{:status 500
|
||||
:body response}))
|
||||
|
||||
;; --- Entry Point
|
||||
|
||||
(defn- handle-data-access-exception
|
||||
[err]
|
||||
(let [err (.getCause err)
|
||||
state (.getSQLState err)
|
||||
message (.getMessage err)]
|
||||
(case state
|
||||
"P0002"
|
||||
{:status 412 ;; precondition-failed
|
||||
:body {:message message
|
||||
:payload nil
|
||||
:type :occ}}
|
||||
|
||||
(do
|
||||
{:status 500
|
||||
:message {:message message
|
||||
:type :unexpected
|
||||
:payload nil}}))))
|
||||
|
||||
(defn- handle-unexpected-exception
|
||||
[err]
|
||||
(let [message (.getMessage err)]
|
||||
{:status 500
|
||||
:body {:message message
|
||||
:type :unexpected
|
||||
:payload nil}}))
|
||||
|
||||
(defn errors-handler
|
||||
[error context]
|
||||
(cond
|
||||
(instance? clojure.lang.ExceptionInfo error)
|
||||
(handle-exception error)
|
||||
|
||||
(instance? java.util.concurrent.CompletionException error)
|
||||
(errors-handler context (.getCause error))
|
||||
|
||||
java.util.concurrent.ExecutionException
|
||||
(errors-handler context (.getCause error))
|
||||
|
||||
(instance? org.jooq.exception.DataAccessException error)
|
||||
(handle-data-access-exception error)
|
||||
|
||||
:else
|
||||
(handle-unexpected-exception error)))
|
||||
|
||||
(defn wrap-print-errors
|
||||
[handler error request]
|
||||
(println "\n*********** stack trace ***********")
|
||||
(.printStackTrace error)
|
||||
(println "\n********* end stack trace *********")
|
||||
(handler error request))
|
185
backend/src/uxbox/http/middleware.clj
Normal file
185
backend/src/uxbox/http/middleware.clj
Normal file
|
@ -0,0 +1,185 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.http.middleware
|
||||
(:require [promesa.core :as p]
|
||||
[cuerdas.core :as str]
|
||||
;; [buddy.core.hash :as hash]
|
||||
;; [buddy.core.codecs :as codecs]
|
||||
;; [buddy.core.codecs.base64 :as b64]
|
||||
[struct.core :as st]
|
||||
[reitit.ring :as rr]
|
||||
[reitit.ring.middleware.multipart :as multipart]
|
||||
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||
[reitit.ring.middleware.parameters :as parameters]
|
||||
[reitit.ring.middleware.exception :as exception]
|
||||
[ring.middleware.session :refer [wrap-session]]
|
||||
[ring.middleware.session.cookie :refer [cookie-store]]
|
||||
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
||||
[uxbox.http.errors :as errors]
|
||||
[uxbox.http.response :as rsp]
|
||||
[uxbox.util.data :refer [normalize-attrs]]
|
||||
[uxbox.util.exceptions :as ex]))
|
||||
|
||||
(defn- transform-handler
|
||||
[handler]
|
||||
(fn [request respond raise]
|
||||
(try
|
||||
(let [response (handler request)]
|
||||
(if (p/promise? response)
|
||||
(-> response
|
||||
(p/then respond)
|
||||
(p/catch raise))
|
||||
(respond response)))
|
||||
(catch Exception e
|
||||
(raise e)))))
|
||||
|
||||
(def ^:private normalize-params-middleware
|
||||
{:name ::normalize-params-middleware
|
||||
:wrap (fn [handler]
|
||||
(letfn [(transform-request [request key]
|
||||
(if-let [data (get request key)]
|
||||
(assoc request key (normalize-attrs data))
|
||||
request))
|
||||
(transform [request]
|
||||
(-> request
|
||||
(transform-request :query-params)
|
||||
(transform-request :multipart-params)))]
|
||||
(fn
|
||||
([request] (handler (transform request)))
|
||||
([request respond raise]
|
||||
(try
|
||||
(try
|
||||
(handler (transform request) respond raise)
|
||||
(catch Exception e
|
||||
(prn handler)
|
||||
(raise e))))))))})
|
||||
|
||||
(def ^:private multipart-params-middleware
|
||||
{:name ::multipart-params-middleware
|
||||
:wrap wrap-multipart-params})
|
||||
|
||||
(def ^:private parameters-validation-middleware
|
||||
(letfn [(prepare [parameters]
|
||||
(reduce-kv
|
||||
(fn [acc key spec]
|
||||
(let [newkey (case key
|
||||
:path :path-params
|
||||
:query :query-params
|
||||
:body :body-params
|
||||
:multipart :multipart-params
|
||||
(throw (ex-info "Not supported key on :parameters" {})))]
|
||||
(assoc acc newkey {:key key
|
||||
:fn #(st/validate % spec)})))
|
||||
{} parameters))
|
||||
|
||||
(validate [request parameters debug]
|
||||
(reduce-kv
|
||||
(fn [req key spec]
|
||||
(let [[errors, result] ((:fn spec) (get req key))]
|
||||
(if errors
|
||||
(ex/raise :type :validation
|
||||
:code (:key spec)
|
||||
:context errors
|
||||
:value (get req key)
|
||||
:message "Invalid data")
|
||||
(assoc-in req [:parameters (:key spec)] result))))
|
||||
request parameters))
|
||||
|
||||
(compile [route opts]
|
||||
(when-let [parameters (:parameters route)]
|
||||
(let [parameters (prepare parameters)]
|
||||
(fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(handler (validate request parameters)))
|
||||
([request respond raise]
|
||||
(try
|
||||
(handler (validate request parameters false) respond raise)
|
||||
(catch Exception e
|
||||
(raise e)))))))))]
|
||||
{:name ::parameters-validation-middleware
|
||||
:compile compile}))
|
||||
|
||||
(def ^:private session-middleware
|
||||
(let [options {:store (cookie-store {:key "a 16-byte secret"})
|
||||
:cookie-name "session"
|
||||
:cookie-attrs {:same-site :lax :http-only true}}]
|
||||
{:name ::session-middleware
|
||||
:wrap #(wrap-session % options)}))
|
||||
|
||||
;; (def ^:private cors-middleware
|
||||
;; {:name ::cors-middleware
|
||||
;; :wrap #(wrap-cors %
|
||||
;; :access-control-allow-origin [#".*"]
|
||||
;; :access-control-allow-methods [:get :put :post :delete]
|
||||
;; :access-control-allow-headers ["x-requested-with"
|
||||
;; "content-type"
|
||||
;; "authorization"])})
|
||||
|
||||
;; (defn digest
|
||||
;; [^bytes data]
|
||||
;; (-> (hash/blake2b-256 data)
|
||||
;; (b64/encode true)
|
||||
;; (codecs/bytes->str)))
|
||||
|
||||
;; (defn- etag-match?
|
||||
;; [^Request request ^String new-tag]
|
||||
;; (let [^Headers headers (.getHeaders request)]
|
||||
;; (when-let [etag (.get headers "if-none-match")]
|
||||
;; (= etag new-tag))))
|
||||
|
||||
(def ^:private exception-middleware
|
||||
(exception/create-exception-middleware
|
||||
(assoc exception/default-handlers
|
||||
::exception/default errors/errors-handler
|
||||
::exception/wrap errors/wrap-print-errors)))
|
||||
|
||||
(def authorization-middleware
|
||||
{:name ::authorization-middleware
|
||||
:wrap (fn [handler]
|
||||
(fn
|
||||
([request]
|
||||
(if-let [identity (get-in request [:session :user-id])]
|
||||
(handler (assoc request :identity identity :user identity))
|
||||
(rsp/forbidden nil)))
|
||||
([request respond raise]
|
||||
(if-let [identity (get-in request [:session :user-id])]
|
||||
(handler (assoc request :identity identity :user identity) respond raise)
|
||||
(respond (rsp/forbidden nil))))))})
|
||||
|
||||
(def middleware
|
||||
[session-middleware
|
||||
parameters/parameters-middleware
|
||||
muuntaja/format-negotiate-middleware
|
||||
;; encoding response body
|
||||
muuntaja/format-response-middleware
|
||||
;; exception handling
|
||||
exception-middleware
|
||||
;; decoding request body
|
||||
muuntaja/format-request-middleware
|
||||
;; multipart
|
||||
multipart-params-middleware
|
||||
;; parameters normalization
|
||||
normalize-params-middleware
|
||||
;; parameters validation
|
||||
parameters-validation-middleware])
|
||||
|
||||
(defn handler
|
||||
[invar]
|
||||
(let [metadata (meta invar)
|
||||
hlrdata (-> metadata
|
||||
(dissoc :arglist :line :column :file :ns)
|
||||
(assoc :handler (transform-handler (var-get invar))
|
||||
:fullname (symbol (str (:ns metadata)) (str (:name metadata)))))]
|
||||
(cond-> hlrdata
|
||||
(:doc metadata) (assoc :description (:doc metadata)))))
|
||||
|
||||
(defn options-handler
|
||||
[request respond raise]
|
||||
(let [methods (->> request rr/get-match :result (keep (fn [[k v]] (if v k))))
|
||||
allow (->> methods (map (comp str/upper name)) (str/join ","))]
|
||||
(respond {:status 200, :body "", :headers {"Allow" allow}})))
|
216
backend/src/uxbox/http/response.clj
Normal file
216
backend/src/uxbox/http/response.clj
Normal file
|
@ -0,0 +1,216 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.http.response)
|
||||
|
||||
(defn response
|
||||
"Create a response instance."
|
||||
([body] (response body 200 {}))
|
||||
([body status] (response body status {}))
|
||||
([body status headers] {:body body :status status :headers headers}))
|
||||
|
||||
(defn response?
|
||||
[resp]
|
||||
(and (map? resp)
|
||||
(integer? (:status resp))
|
||||
(map? (:headers resp))))
|
||||
|
||||
(defn continue
|
||||
([body] (response body 100))
|
||||
([body headers] (response body 100 headers)))
|
||||
|
||||
(defn ok
|
||||
"HTTP 200 OK
|
||||
Should be used to indicate nonspecific success. Must not be used to
|
||||
communicate errors in the response body.
|
||||
|
||||
In most cases, 200 is the code the client hopes to see. It indicates that
|
||||
the REST API successfully carried out whatever action the client requested,
|
||||
and that no more specific code in the 2xx series is appropriate. Unlike
|
||||
the 204 status code, a 200 response should include a response body."
|
||||
([body] (response body 200))
|
||||
([body headers] (response body 200 headers)))
|
||||
|
||||
(defn created
|
||||
"HTTP 201 Created
|
||||
Must be used to indicate successful resource creation.
|
||||
|
||||
A REST API responds with the 201 status code whenever a collection creates,
|
||||
or a store adds, a new resource at the client's request. There may also be
|
||||
times when a new resource is created as a result of some controller action,
|
||||
in which case 201 would also be an appropriate response."
|
||||
([location] (response "" 201 {"location" location}))
|
||||
([location body] (response body 201 {"location" location}))
|
||||
([location body headers] (response body 201 (merge headers {"location" location}))))
|
||||
|
||||
(defn accepted
|
||||
"HTTP 202 Accepted
|
||||
Must be used to indicate successful start of an asynchronous action.
|
||||
|
||||
A 202 response indicates that the client's request will be handled
|
||||
asynchronously. This response status code tells the client that the request
|
||||
appears valid, but it still may have problems once it's finally processed.
|
||||
A 202 response is typically used for actions that take a long while to
|
||||
process."
|
||||
([body] (response body 202))
|
||||
([body headers] (response body 202 headers)))
|
||||
|
||||
(defn no-content
|
||||
"HTTP 204 No Content
|
||||
Should be used when the response body is intentionally empty.
|
||||
|
||||
The 204 status code is usually sent out in response to a PUT, POST, or
|
||||
DELETE request, when the REST API declines to send back any status message
|
||||
or representation in the response message's body. An API may also send 204
|
||||
in conjunction with a GET request to indicate that the requested resource
|
||||
exists, but has no state representation to include in the body."
|
||||
([] (response "" 204))
|
||||
([headers] (response "" 204 headers)))
|
||||
|
||||
(defn moved-permanently
|
||||
"301 Moved Permanently
|
||||
Should be used to relocate resources.
|
||||
|
||||
The 301 status code indicates that the REST API's resource model has been
|
||||
significantly redesigned and a new permanent URI has been assigned to the
|
||||
client's requested resource. The REST API should specify the new URI in
|
||||
the response's Location header."
|
||||
([location] (response "" 301 {"location" location}))
|
||||
([location body] (response body 301 {"location" location}))
|
||||
([location body headers] (response body 301 (merge headers {"location" location}))))
|
||||
|
||||
(defn found
|
||||
"HTTP 302 Found
|
||||
Should not be used.
|
||||
|
||||
The intended semantics of the 302 response code have been misunderstood
|
||||
by programmers and incorrectly implemented in programs since version 1.0
|
||||
of the HTTP protocol.
|
||||
The confusion centers on whether it is appropriate for a client to always
|
||||
automatically issue a follow-up GET request to the URI in response's
|
||||
Location header, regardless of the original request's method. For the
|
||||
record, the intent of 302 is that this automatic redirect behavior only
|
||||
applies if the client's original request used either the GET or HEAD
|
||||
method.
|
||||
|
||||
To clear things up, HTTP 1.1 introduced status codes 303 (\"See Other\")
|
||||
and 307 (\"Temporary Redirect\"), either of which should be used
|
||||
instead of 302."
|
||||
([location] (response "" 302 {"location" location}))
|
||||
([location body] (response body 302 {"location" location}))
|
||||
([location body headers] (response body 302 (merge headers {"location" location}))))
|
||||
|
||||
(defn see-other
|
||||
"HTTP 303 See Other
|
||||
Should be used to refer the client to a different URI.
|
||||
|
||||
A 303 response indicates that a controller resource has finished its work,
|
||||
but instead of sending a potentially unwanted response body, it sends the
|
||||
client the URI of a response resource. This can be the URI of a temporary
|
||||
status message, or the URI to some already existing, more permanent,
|
||||
resource.
|
||||
Generally speaking, the 303 status code allows a REST API to send a
|
||||
reference to a resource without forcing the client to download its state.
|
||||
Instead, the client may send a GET request to the value of the Location
|
||||
header."
|
||||
([location] (response "" 303 {"location" location}))
|
||||
([location body] (response body 303 {"location" location}))
|
||||
([location body headers] (response body 303 (merge headers {"location" location}))))
|
||||
|
||||
(defn temporary-redirect
|
||||
"HTTP 307 Temporary Redirect
|
||||
Should be used to tell clients to resubmit the request to another URI.
|
||||
|
||||
HTTP/1.1 introduced the 307 status code to reiterate the originally
|
||||
intended semantics of the 302 (\"Found\") status code. A 307 response
|
||||
indicates that the REST API is not going to process the client's request.
|
||||
Instead, the client should resubmit the request to the URI specified by
|
||||
the response message's Location header.
|
||||
|
||||
A REST API can use this status code to assign a temporary URI to the
|
||||
client's requested resource. For example, a 307 response can be used to
|
||||
shift a client request over to another host."
|
||||
([location] (response "" 307 {"location" location}))
|
||||
([location body] (response body 307 {"location" location}))
|
||||
([location body headers] (response body 307 (merge headers {"location" location}))))
|
||||
|
||||
(defn bad-request
|
||||
"HTTP 400 Bad Request
|
||||
May be used to indicate nonspecific failure.
|
||||
|
||||
400 is the generic client-side error status, used when no other 4xx error
|
||||
code is appropriate."
|
||||
([body] (response body 400))
|
||||
([body headers] (response body 400 headers)))
|
||||
|
||||
(defn unauthorized
|
||||
"HTTP 401 Unauthorized
|
||||
Must be used when there is a problem with the client credentials.
|
||||
|
||||
A 401 error response indicates that the client tried to operate on a
|
||||
protected resource without providing the proper authorization. It may have
|
||||
provided the wrong credentials or none at all."
|
||||
([body] (response body 401))
|
||||
([body headers] (response body 401 headers)))
|
||||
|
||||
(defn forbidden
|
||||
"HTTP 403 Forbidden
|
||||
Should be used to forbid access regardless of authorization state.
|
||||
|
||||
A 403 error response indicates that the client's request is formed
|
||||
correctly, but the REST API refuses to honor it. A 403 response is not a
|
||||
case of insufficient client credentials; that would be 401 (\"Unauthorized\").
|
||||
REST APIs use 403 to enforce application-level permissions. For example, a
|
||||
client may be authorized to interact with some, but not all of a REST API's
|
||||
resources. If the client attempts a resource interaction that is outside of
|
||||
its permitted scope, the REST API should respond with 403."
|
||||
([body] (response body 403))
|
||||
([body headers] (response body 403 headers)))
|
||||
|
||||
(defn not-found
|
||||
"HTTP 404 Not Found
|
||||
Must be used when a client's URI cannot be mapped to a resource.
|
||||
|
||||
The 404 error status code indicates that the REST API can't map the
|
||||
client's URI to a resource."
|
||||
([body] (response body 404))
|
||||
([body headers] (response body 404 headers)))
|
||||
|
||||
(defn method-not-allowed
|
||||
([body] (response body 405))
|
||||
([body headers] (response body 405 headers)))
|
||||
|
||||
(defn not-acceptable
|
||||
([body] (response body 406))
|
||||
([body headers] (response body 406 headers)))
|
||||
|
||||
(defn conflict
|
||||
([body] (response body 409))
|
||||
([body headers] (response body 409 headers)))
|
||||
|
||||
(defn gone
|
||||
([body] (response body 410))
|
||||
([body headers] (response body 410 headers)))
|
||||
|
||||
(defn precondition-failed
|
||||
([body] (response body 412))
|
||||
([body headers] (response body 412 headers)))
|
||||
|
||||
(defn unsupported-mediatype
|
||||
([body] (response body 415))
|
||||
([body headers] (response body 415 headers)))
|
||||
|
||||
(defn too-many-requests
|
||||
([body] (response body 429))
|
||||
([body headers] (response body 429 headers)))
|
||||
|
||||
(defn internal-server-error
|
||||
([body] (response body 500))
|
||||
([body headers] (response body 500 headers)))
|
||||
|
||||
(defn not-implemented
|
||||
([body] (response body 501))
|
||||
([body headers] (response body 501 headers)))
|
Loading…
Add table
Add a link
Reference in a new issue