mirror of
https://github.com/penpot/penpot.git
synced 2025-06-15 16:41:43 +02:00
🎉 Add authentication with google.
This commit is contained in:
parent
5268a7663f
commit
19cd84597d
23 changed files with 589 additions and 276 deletions
14
backend/resources/migrations/0010-add-http-session-table.sql
Normal file
14
backend/resources/migrations/0010-add-http-session-table.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
DROP TABLE session;
|
||||||
|
|
||||||
|
CREATE TABLE http_session (
|
||||||
|
id text PRIMARY KEY,
|
||||||
|
|
||||||
|
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||||
|
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||||
|
|
||||||
|
profile_id uuid REFERENCES profile(id) ON DELETE CASCADE,
|
||||||
|
user_agent text NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX http_session__profile_id__idx
|
||||||
|
ON http_session(profile_id);
|
|
@ -26,7 +26,8 @@
|
||||||
:database-username "uxbox"
|
:database-username "uxbox"
|
||||||
:database-password "uxbox"
|
:database-password "uxbox"
|
||||||
|
|
||||||
:public-url "http://localhost:3449"
|
:public-uri "http://localhost:3449"
|
||||||
|
:backend-uri "http://localhost:6060"
|
||||||
|
|
||||||
:redis-uri "redis://redis/0"
|
:redis-uri "redis://redis/0"
|
||||||
:media-directory "resources/public/media"
|
:media-directory "resources/public/media"
|
||||||
|
@ -69,13 +70,19 @@
|
||||||
(s/def ::registration-enabled ::us/boolean)
|
(s/def ::registration-enabled ::us/boolean)
|
||||||
(s/def ::registration-domain-whitelist ::us/string)
|
(s/def ::registration-domain-whitelist ::us/string)
|
||||||
(s/def ::debug-humanize-transit ::us/boolean)
|
(s/def ::debug-humanize-transit ::us/boolean)
|
||||||
(s/def ::public-url ::us/string)
|
(s/def ::public-uri ::us/string)
|
||||||
|
(s/def ::backend-uri ::us/string)
|
||||||
|
|
||||||
|
(s/def ::google-client-id ::us/string)
|
||||||
|
(s/def ::google-client-secret ::us/string)
|
||||||
|
|
||||||
(s/def ::config
|
(s/def ::config
|
||||||
(s/keys :opt-un [::http-server-cors
|
(s/keys :opt-un [::http-server-cors
|
||||||
::http-server-debug
|
::http-server-debug
|
||||||
::http-server-port
|
::http-server-port
|
||||||
::public-url
|
::google-client-id
|
||||||
|
::google-client-secret
|
||||||
|
::public-uri
|
||||||
::database-username
|
::database-username
|
||||||
::database-password
|
::database-password
|
||||||
::database-uri
|
::database-uri
|
||||||
|
|
|
@ -75,8 +75,10 @@
|
||||||
(jdbc/get-connection pool))
|
(jdbc/get-connection pool))
|
||||||
|
|
||||||
(defn exec!
|
(defn exec!
|
||||||
[ds sv]
|
([ds sv]
|
||||||
(jdbc/execute! ds sv {:builder-fn as-kebab-maps}))
|
(exec! ds sv {}))
|
||||||
|
([ds sv opts]
|
||||||
|
(jdbc/execute! ds sv (assoc opts :builder-fn as-kebab-maps))))
|
||||||
|
|
||||||
(defn exec-one!
|
(defn exec-one!
|
||||||
([ds sv] (exec-one! ds sv {}))
|
([ds sv] (exec-one! ds sv {}))
|
||||||
|
@ -120,6 +122,15 @@
|
||||||
([ds table id opts]
|
([ds table id opts]
|
||||||
(get-by-params ds table {:id id} opts)))
|
(get-by-params ds table {:id id} opts)))
|
||||||
|
|
||||||
|
(defn query
|
||||||
|
([ds table params]
|
||||||
|
(query ds table params nil))
|
||||||
|
([ds table params opts]
|
||||||
|
(let [opts (cond-> (merge default-options opts)
|
||||||
|
(:for-update opts)
|
||||||
|
(assoc :suffix "for update"))]
|
||||||
|
(exec! ds (jdbc-bld/for-query table params opts) opts))))
|
||||||
|
|
||||||
(defn pgobject?
|
(defn pgobject?
|
||||||
[v]
|
[v]
|
||||||
(instance? PGobject v))
|
(instance? PGobject v))
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.emails
|
(ns uxbox.emails
|
||||||
"Main api for send emails."
|
"Main api for send emails."
|
||||||
|
@ -63,7 +66,7 @@
|
||||||
"A password recovery notification email."
|
"A password recovery notification email."
|
||||||
(emails/build ::password-recovery default-context))
|
(emails/build ::password-recovery default-context))
|
||||||
|
|
||||||
(s/def ::pending-email ::us/string)
|
(s/def ::pending-email ::us/email)
|
||||||
(s/def ::change-email
|
(s/def ::change-email
|
||||||
(s/keys :req-un [::name ::pending-email ::token]))
|
(s/keys :req-un [::name ::pending-email ::token]))
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
[uxbox.db :as db]
|
[uxbox.db :as db]
|
||||||
[uxbox.media :as media]
|
[uxbox.media :as media]
|
||||||
[uxbox.migrations]
|
[uxbox.migrations]
|
||||||
[uxbox.services.mutations.profile :as mt.profile]
|
[uxbox.services.mutations.profile :as profile]
|
||||||
[uxbox.util.blob :as blob]))
|
[uxbox.util.blob :as blob]))
|
||||||
|
|
||||||
(defn- mk-uuid
|
(defn- mk-uuid
|
||||||
|
@ -66,6 +66,11 @@
|
||||||
[f items]
|
[f items]
|
||||||
(reduce #(conj %1 (f %2)) [] items))
|
(reduce #(conj %1 (f %2)) [] items))
|
||||||
|
|
||||||
|
(defn- register-profile
|
||||||
|
[conn params]
|
||||||
|
(->> (#'profile/create-profile conn params)
|
||||||
|
(#'profile/create-profile-relations conn)))
|
||||||
|
|
||||||
(defn impl-run
|
(defn impl-run
|
||||||
[opts]
|
[opts]
|
||||||
(let [rng (java.util.Random. 1)
|
(let [rng (java.util.Random. 1)
|
||||||
|
@ -74,12 +79,12 @@
|
||||||
(fn [conn index]
|
(fn [conn index]
|
||||||
(let [id (mk-uuid "profile" index)]
|
(let [id (mk-uuid "profile" index)]
|
||||||
(log/info "create profile" id)
|
(log/info "create profile" id)
|
||||||
(mt.profile/register-profile conn
|
(register-profile conn
|
||||||
{:id id
|
{:id id
|
||||||
:fullname (str "Profile " index)
|
:fullname (str "Profile " index)
|
||||||
:password "123123"
|
:password "123123"
|
||||||
:demo? true
|
:demo? true
|
||||||
:email (str "profile" index ".test@uxbox.io")})))
|
:email (str "profile" index ".test@uxbox.io")})))
|
||||||
|
|
||||||
create-profiles
|
create-profiles
|
||||||
(fn [conn]
|
(fn [conn]
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.http
|
(ns uxbox.http
|
||||||
(:require
|
(:require
|
||||||
|
@ -14,6 +17,8 @@
|
||||||
[uxbox.http.debug :as debug]
|
[uxbox.http.debug :as debug]
|
||||||
[uxbox.http.errors :as errors]
|
[uxbox.http.errors :as errors]
|
||||||
[uxbox.http.handlers :as handlers]
|
[uxbox.http.handlers :as handlers]
|
||||||
|
[uxbox.http.auth :as auth]
|
||||||
|
[uxbox.http.auth.google :as google]
|
||||||
[uxbox.http.middleware :as middleware]
|
[uxbox.http.middleware :as middleware]
|
||||||
[uxbox.http.session :as session]
|
[uxbox.http.session :as session]
|
||||||
[uxbox.http.ws :as ws]
|
[uxbox.http.ws :as ws]
|
||||||
|
@ -31,12 +36,17 @@
|
||||||
[middleware/multipart-params]
|
[middleware/multipart-params]
|
||||||
[middleware/keyword-params]
|
[middleware/keyword-params]
|
||||||
[middleware/cookies]]}
|
[middleware/cookies]]}
|
||||||
|
|
||||||
|
["/oauth"
|
||||||
|
["/google" {:post google/auth}]
|
||||||
|
["/google/callback" {:get google/callback}]]
|
||||||
|
|
||||||
["/echo" {:get handlers/echo-handler
|
["/echo" {:get handlers/echo-handler
|
||||||
:post handlers/echo-handler}]
|
:post handlers/echo-handler}]
|
||||||
|
|
||||||
["/login" {:handler handlers/login-handler
|
["/login" {:handler auth/login-handler
|
||||||
:method :post}]
|
:method :post}]
|
||||||
["/logout" {:handler handlers/logout-handler
|
["/logout" {:handler auth/logout-handler
|
||||||
:method :post}]
|
:method :post}]
|
||||||
|
|
||||||
["/w" {:middleware [session/auth]}
|
["/w" {:middleware [session/auth]}
|
||||||
|
|
33
backend/src/uxbox/http/auth.clj
Normal file
33
backend/src/uxbox/http/auth.clj
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns uxbox.http.auth
|
||||||
|
(:require
|
||||||
|
[uxbox.common.exceptions :as ex]
|
||||||
|
[uxbox.common.uuid :as uuid]
|
||||||
|
[uxbox.http.session :as session]
|
||||||
|
[uxbox.services.mutations :as sm]))
|
||||||
|
|
||||||
|
(defn login-handler
|
||||||
|
[req]
|
||||||
|
(let [data (:body-params req)
|
||||||
|
uagent (get-in req [:headers "user-agent"])]
|
||||||
|
(let [profile (sm/handle (assoc data ::sm/type :login))
|
||||||
|
id (session/create (:id profile) uagent)]
|
||||||
|
{:status 200
|
||||||
|
:cookies (session/cookies id)
|
||||||
|
:body profile})))
|
||||||
|
|
||||||
|
(defn logout-handler
|
||||||
|
[req]
|
||||||
|
(some-> (session/extract-auth-token req)
|
||||||
|
(session/delete))
|
||||||
|
{:status 200
|
||||||
|
:cookies (session/cookies "" {:max-age -1})
|
||||||
|
:body ""})
|
133
backend/src/uxbox/http/auth/google.clj
Normal file
133
backend/src/uxbox/http/auth/google.clj
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
;; 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.auth.google
|
||||||
|
(:require
|
||||||
|
[clojure.data.json :as json]
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[lambdaisland.uri :as uri]
|
||||||
|
[uxbox.common.exceptions :as ex]
|
||||||
|
[uxbox.config :as cfg]
|
||||||
|
[uxbox.db :as db]
|
||||||
|
[uxbox.services.tokens :as tokens]
|
||||||
|
[uxbox.services.mutations :as sm]
|
||||||
|
[uxbox.http.session :as session]
|
||||||
|
[uxbox.util.http :as http]))
|
||||||
|
|
||||||
|
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
|
||||||
|
|
||||||
|
(def scope
|
||||||
|
(str "email profile "
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email "
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile "
|
||||||
|
"openid"))
|
||||||
|
|
||||||
|
(defn- build-redirect-url
|
||||||
|
[]
|
||||||
|
(let [public (uri/uri (:backend-uri cfg/config))]
|
||||||
|
(str (assoc public :path "/api/oauth/google/callback"))))
|
||||||
|
|
||||||
|
(defn- get-access-token
|
||||||
|
[code]
|
||||||
|
(let [params {:code code
|
||||||
|
:client_id (:google-client-id cfg/config)
|
||||||
|
:client_secret (:google-client-secret cfg/config)
|
||||||
|
:redirect_uri (build-redirect-url)
|
||||||
|
:grant_type "authorization_code"}
|
||||||
|
req {:method :post
|
||||||
|
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||||
|
:uri "https://oauth2.googleapis.com/token"
|
||||||
|
:body (uri/map->query-string params)}
|
||||||
|
res (http/send! req)]
|
||||||
|
|
||||||
|
(when (not= 200 (:status res))
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-response-from-google
|
||||||
|
:context {:status (:status res)
|
||||||
|
:body (:body res)}))
|
||||||
|
|
||||||
|
(try
|
||||||
|
(let [data (json/read-str (:body res))]
|
||||||
|
(get data "access_token"))
|
||||||
|
(catch Throwable e
|
||||||
|
(log/error "unexpected error on parsing response body from google access tooken request" e)
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- get-user-info
|
||||||
|
[token]
|
||||||
|
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||||
|
:headers {"Authorization" (str "Bearer " token)}
|
||||||
|
:method :get}
|
||||||
|
res (http/send! req)]
|
||||||
|
|
||||||
|
(when (not= 200 (:status res))
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :invalid-response-from-google
|
||||||
|
:context {:status (:status res)
|
||||||
|
:body (:body res)}))
|
||||||
|
|
||||||
|
(try
|
||||||
|
(let [data (json/read-str (:body res))]
|
||||||
|
;; (clojure.pprint/pprint data)
|
||||||
|
{:email (get data "email")
|
||||||
|
:fullname (get data "name")})
|
||||||
|
(catch Throwable e
|
||||||
|
(log/error "unexpected error on parsing response body from google access tooken request" e)
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
(defn auth
|
||||||
|
[req]
|
||||||
|
(let [token (tokens/create! db/pool {:type :google-oauth})
|
||||||
|
params {:scope scope
|
||||||
|
:access_type "offline"
|
||||||
|
:include_granted_scopes true
|
||||||
|
:state token
|
||||||
|
:response_type "code"
|
||||||
|
:redirect_uri (build-redirect-url)
|
||||||
|
:client_id (:google-client-id cfg/config)}
|
||||||
|
query (uri/map->query-string params)
|
||||||
|
uri (-> (uri/uri base-goauth-uri)
|
||||||
|
(assoc :query query))]
|
||||||
|
{:status 200
|
||||||
|
:body {:redirect-uri (str uri)}}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn callback
|
||||||
|
[req]
|
||||||
|
(let [token (get-in req [:params :state])
|
||||||
|
tdata (tokens/retrieve db/pool token)
|
||||||
|
info (some-> (get-in req [:params :code])
|
||||||
|
(get-access-token)
|
||||||
|
(get-user-info))]
|
||||||
|
|
||||||
|
(when (not= :google-oauth (:type tdata))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code ::tokens/invalid-token))
|
||||||
|
|
||||||
|
(when-not info
|
||||||
|
(ex/raise :type :authentication
|
||||||
|
:code ::unable-to-authenticate-with-google))
|
||||||
|
|
||||||
|
(let [profile (sm/handle {::sm/type :login-or-register
|
||||||
|
:email (:email info)
|
||||||
|
:fullname (:fullname info)})
|
||||||
|
uagent (get-in req [:headers "user-agent"])
|
||||||
|
|
||||||
|
tdata {:type :authentication
|
||||||
|
:profile profile}
|
||||||
|
token (tokens/create! db/pool tdata {:valid {:minutes 10}})
|
||||||
|
|
||||||
|
uri (-> (uri/uri (:public-uri cfg/config))
|
||||||
|
(assoc :path "/#/auth/verify-token")
|
||||||
|
(assoc :query (uri/map->query-string {:token token})))
|
||||||
|
sid (session/create (:id profile) uagent)]
|
||||||
|
|
||||||
|
{:status 302
|
||||||
|
:headers {"location" (str uri)}
|
||||||
|
:cookies (session/cookies sid)
|
||||||
|
:body ""})))
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.http.handlers
|
(ns uxbox.http.handlers
|
||||||
(:require
|
(:require
|
||||||
[uxbox.common.exceptions :as ex]
|
[uxbox.common.exceptions :as ex]
|
||||||
[uxbox.common.uuid :as uuid]
|
|
||||||
[uxbox.emails :as emails]
|
[uxbox.emails :as emails]
|
||||||
[uxbox.http.session :as session]
|
[uxbox.http.session :as session]
|
||||||
[uxbox.services.init]
|
[uxbox.services.init]
|
||||||
|
@ -54,11 +56,10 @@
|
||||||
(let [body (sm/handle (with-meta data {:req req}))]
|
(let [body (sm/handle (with-meta data {:req req}))]
|
||||||
(if (= type :delete-profile)
|
(if (= type :delete-profile)
|
||||||
(do
|
(do
|
||||||
(some-> (get-in req [:cookies "auth-token" :value])
|
(some-> (session/extract-auth-token req)
|
||||||
(uuid/uuid)
|
|
||||||
(session/delete))
|
(session/delete))
|
||||||
{:status 204
|
{:status 204
|
||||||
:cookies {"auth-token" {:value "" :max-age -1}}
|
:cookies (session/cookies "" {:max-age -1})
|
||||||
:body ""})
|
:body ""})
|
||||||
{:status 200
|
{:status 200
|
||||||
:body body}))
|
:body body}))
|
||||||
|
@ -66,25 +67,6 @@
|
||||||
:body {:type :authentication
|
:body {:type :authentication
|
||||||
:code :unauthorized}})))
|
:code :unauthorized}})))
|
||||||
|
|
||||||
(defn login-handler
|
|
||||||
[req]
|
|
||||||
(let [data (:body-params req)
|
|
||||||
user-agent (get-in req [:headers "user-agent"])]
|
|
||||||
(let [profile (sm/handle (assoc data ::sm/type :login))
|
|
||||||
token (session/create (:id profile) user-agent)]
|
|
||||||
{:status 200
|
|
||||||
:cookies {"auth-token" {:value token :path "/"}}
|
|
||||||
:body profile})))
|
|
||||||
|
|
||||||
(defn logout-handler
|
|
||||||
[req]
|
|
||||||
(some-> (get-in req [:cookies "auth-token" :value])
|
|
||||||
(uuid/uuid)
|
|
||||||
(session/delete))
|
|
||||||
{:status 200
|
|
||||||
:cookies {"auth-token" {:value "" :max-age -1}}
|
|
||||||
:body ""})
|
|
||||||
|
|
||||||
(defn echo-handler
|
(defn echo-handler
|
||||||
[req]
|
[req]
|
||||||
{:status 200
|
{:status 200
|
||||||
|
|
|
@ -2,49 +2,51 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.http.session
|
(ns uxbox.http.session
|
||||||
(:require
|
(:require
|
||||||
[uxbox.db :as db]
|
[uxbox.db :as db]
|
||||||
|
[uxbox.services.tokens :as tokens]
|
||||||
[uxbox.common.uuid :as uuid]))
|
[uxbox.common.uuid :as uuid]))
|
||||||
|
|
||||||
;; --- Main API
|
|
||||||
|
|
||||||
(defn retrieve
|
(defn retrieve
|
||||||
"Retrieves a user id associated with the provided auth token."
|
"Retrieves a user id associated with the provided auth token."
|
||||||
[token]
|
[token]
|
||||||
(when token
|
(when token
|
||||||
(let [row (db/get-by-params db/pool :session {:id token})]
|
(-> (db/query db/pool :http-session {:id token})
|
||||||
(:profile-id row))))
|
(first)
|
||||||
|
(:profile-id))))
|
||||||
|
|
||||||
(defn create
|
(defn create
|
||||||
[user-id user-agent]
|
[profile-id user-agent]
|
||||||
(let [id (uuid/random)]
|
(let [id (tokens/next)]
|
||||||
(db/insert! db/pool :session {:id id
|
(db/insert! db/pool :http-session {:id id
|
||||||
:profile-id user-id
|
:profile-id profile-id
|
||||||
:user-agent user-agent})
|
:user-agent user-agent})
|
||||||
(str id)))
|
id))
|
||||||
|
|
||||||
(defn delete
|
(defn delete
|
||||||
[token]
|
[token]
|
||||||
(db/delete! db/pool :session {:id token})
|
(db/delete! db/pool :http-session {:id token})
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
;; --- Interceptor
|
(defn cookies
|
||||||
|
([id] (cookies id {}))
|
||||||
|
([id opts]
|
||||||
|
{"auth-token" (merge opts {:value id :path "/" :http-only true})}))
|
||||||
|
|
||||||
(defn- parse-token
|
(defn extract-auth-token
|
||||||
[request]
|
[req]
|
||||||
(try
|
(get-in req [:cookies "auth-token" :value]))
|
||||||
(when-let [token (get-in request [:cookies "auth-token"])]
|
|
||||||
(uuid/uuid (:value token)))
|
|
||||||
(catch java.lang.IllegalArgumentException e
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
(defn wrap-auth
|
(defn wrap-auth
|
||||||
[handler]
|
[handler]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(let [token (parse-token request)
|
(let [token (get-in request [:cookies "auth-token" :value])
|
||||||
profile-id (retrieve token)]
|
profile-id (retrieve token)]
|
||||||
(if profile-id
|
(if profile-id
|
||||||
(handler (assoc request :profile-id profile-id))
|
(handler (assoc request :profile-id profile-id))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
;; defined by the Mozilla Public License, v. 2.0.
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz>
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.main
|
(ns uxbox.main
|
||||||
(:require
|
(:require
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.migrations
|
(ns uxbox.migrations
|
||||||
(:require
|
(:require
|
||||||
|
@ -44,12 +47,16 @@
|
||||||
:fn (mg/resource "migrations/0007-drop-version-field-from-page-table.sql")}
|
:fn (mg/resource "migrations/0007-drop-version-field-from-page-table.sql")}
|
||||||
|
|
||||||
{:desc "Add generic token related tables."
|
{:desc "Add generic token related tables."
|
||||||
:name "0008-add-generic-token-table.sql"
|
:name "0008-add-generic-token-table"
|
||||||
:fn (mg/resource "migrations/0008-add-generic-token-table.sql")}
|
:fn (mg/resource "migrations/0008-add-generic-token-table.sql")}
|
||||||
|
|
||||||
{:desc "Drop the profile_email table"
|
{:desc "Drop the profile_email table"
|
||||||
:name "0009-drop-profile-email-table.sql"
|
:name "0009-drop-profile-email-table"
|
||||||
:fn (mg/resource "migrations/0009-drop-profile-email-table.sql")}]})
|
:fn (mg/resource "migrations/0009-drop-profile-email-table.sql")}
|
||||||
|
|
||||||
|
{:desc "Add new HTTP session table"
|
||||||
|
:name "0010-add-http-session-table"
|
||||||
|
:fn (mg/resource "migrations/0010-add-http-session-table.sql")}]})
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Entry point
|
;; Entry point
|
||||||
|
|
|
@ -29,13 +29,15 @@
|
||||||
email (str "demo-" sem ".demo@nodomain.com")
|
email (str "demo-" sem ".demo@nodomain.com")
|
||||||
fullname (str "Demo User " sem)
|
fullname (str "Demo User " sem)
|
||||||
password (-> (sodi.prng/random-bytes 12)
|
password (-> (sodi.prng/random-bytes 12)
|
||||||
(sodi.util/bytes->b64s))]
|
(sodi.util/bytes->b64s))
|
||||||
|
params {:id id
|
||||||
|
:email email
|
||||||
|
:fullname fullname
|
||||||
|
:demo? true
|
||||||
|
:password password}]
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
(#'profile/register-profile conn {:id id
|
(->> (#'profile/create-profile conn params)
|
||||||
:email email
|
(#'profile/create-profile-relations conn))
|
||||||
:fullname fullname
|
|
||||||
:demo? true
|
|
||||||
:password password})
|
|
||||||
|
|
||||||
;; Schedule deletion of the demo profile
|
;; Schedule deletion of the demo profile
|
||||||
(tasks/submit! conn {:name "delete-profile"
|
(tasks/submit! conn {:name "delete-profile"
|
||||||
|
|
|
@ -25,10 +25,11 @@
|
||||||
[uxbox.emails :as emails]
|
[uxbox.emails :as emails]
|
||||||
[uxbox.images :as images]
|
[uxbox.images :as images]
|
||||||
[uxbox.media :as media]
|
[uxbox.media :as media]
|
||||||
|
[uxbox.services.tokens :as tokens]
|
||||||
[uxbox.services.mutations :as sm]
|
[uxbox.services.mutations :as sm]
|
||||||
[uxbox.services.mutations.images :as imgs]
|
[uxbox.services.mutations.images :as imgs]
|
||||||
[uxbox.services.mutations.projects :as mt.projects]
|
[uxbox.services.mutations.projects :as projects]
|
||||||
[uxbox.services.mutations.teams :as mt.teams]
|
[uxbox.services.mutations.teams :as teams]
|
||||||
[uxbox.services.queries.profile :as profile]
|
[uxbox.services.queries.profile :as profile]
|
||||||
[uxbox.tasks :as tasks]
|
[uxbox.tasks :as tasks]
|
||||||
[uxbox.util.blob :as blob]
|
[uxbox.util.blob :as blob]
|
||||||
|
@ -46,12 +47,100 @@
|
||||||
(s/def ::old-password ::us/string)
|
(s/def ::old-password ::us/string)
|
||||||
(s/def ::theme ::us/string)
|
(s/def ::theme ::us/string)
|
||||||
|
|
||||||
(defn decode-token-row
|
;; --- Mutation: Register Profile
|
||||||
[{:keys [content] :as row}]
|
|
||||||
(when row
|
|
||||||
(cond-> row
|
|
||||||
content (assoc :content (blob/decode content)))))
|
|
||||||
|
|
||||||
|
(declare check-profile-existence!)
|
||||||
|
(declare create-profile)
|
||||||
|
(declare create-profile-relations)
|
||||||
|
|
||||||
|
(s/def ::register-profile
|
||||||
|
(s/keys :req-un [::email ::password ::fullname]))
|
||||||
|
|
||||||
|
(defn email-domain-in-whitelist?
|
||||||
|
"Returns true if email's domain is in the given whitelist or if given
|
||||||
|
whitelist is an empty string."
|
||||||
|
[whitelist email]
|
||||||
|
(if (str/blank? whitelist)
|
||||||
|
true
|
||||||
|
(let [domains (str/split whitelist #",\s*")
|
||||||
|
email-domain (second (str/split email #"@"))]
|
||||||
|
(contains? (set domains) email-domain))))
|
||||||
|
|
||||||
|
(sm/defmutation ::register-profile
|
||||||
|
[params]
|
||||||
|
(when-not (:registration-enabled cfg/config)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code ::registration-disabled))
|
||||||
|
|
||||||
|
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
|
||||||
|
(:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code ::email-domain-is-not-allowed))
|
||||||
|
|
||||||
|
(db/with-atomic [conn db/pool]
|
||||||
|
(check-profile-existence! conn params)
|
||||||
|
(let [profile (->> (create-profile conn params)
|
||||||
|
(create-profile-relations conn))
|
||||||
|
payload {:type :verify-email
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email (:email profile)}
|
||||||
|
|
||||||
|
token (tokens/create! conn payload {:valid {:days 30}})]
|
||||||
|
|
||||||
|
(emails/send! conn emails/register
|
||||||
|
{:to (:email profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:public-url (:public-uri cfg/config)
|
||||||
|
:token token})
|
||||||
|
profile)))
|
||||||
|
|
||||||
|
(def ^:private sql:profile-existence
|
||||||
|
"select exists (select * from profile
|
||||||
|
where email = ?
|
||||||
|
and deleted_at is null) as val")
|
||||||
|
|
||||||
|
(defn- check-profile-existence!
|
||||||
|
[conn {:keys [email] :as params}]
|
||||||
|
(let [email (str/lower email)
|
||||||
|
result (db/exec-one! conn [sql:profile-existence email])]
|
||||||
|
(when (:val result)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code ::email-already-exists))
|
||||||
|
params))
|
||||||
|
|
||||||
|
(defn- create-profile
|
||||||
|
"Create the profile entry on the database with limited input
|
||||||
|
filling all the other fields with defaults."
|
||||||
|
[conn {:keys [id fullname email password demo?] :as params}]
|
||||||
|
(let [id (or id (uuid/next))
|
||||||
|
demo? (if (boolean? demo?) demo? false)
|
||||||
|
password (sodi.pwhash/derive password)]
|
||||||
|
(db/insert! conn :profile
|
||||||
|
{:id id
|
||||||
|
:fullname fullname
|
||||||
|
:email (str/lower email)
|
||||||
|
:pending-email (if demo? nil email)
|
||||||
|
:photo ""
|
||||||
|
:password password
|
||||||
|
:is-demo demo?})))
|
||||||
|
|
||||||
|
(defn- create-profile-relations
|
||||||
|
[conn profile]
|
||||||
|
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||||
|
:name "Default"
|
||||||
|
:default? true})
|
||||||
|
proj (projects/create-project conn {:profile-id (:id profile)
|
||||||
|
:team-id (:id team)
|
||||||
|
:name "Drafts"
|
||||||
|
:default? true})]
|
||||||
|
(teams/create-team-profile conn {:team-id (:id team)
|
||||||
|
:profile-id (:id profile)})
|
||||||
|
(projects/create-project-profile conn {:project-id (:id proj)
|
||||||
|
:profile-id (:id profile)})
|
||||||
|
|
||||||
|
(merge (profile/strip-private-attrs profile)
|
||||||
|
{:default-team-id (:id team)
|
||||||
|
:default-project-id (:id proj)})))
|
||||||
|
|
||||||
;; --- Mutation: Login
|
;; --- Mutation: Login
|
||||||
|
|
||||||
|
@ -70,7 +159,7 @@
|
||||||
(let [result (sodi.pwhash/verify password (:password profile))]
|
(let [result (sodi.pwhash/verify password (:password profile))]
|
||||||
(:valid result)))
|
(:valid result)))
|
||||||
|
|
||||||
(check-profile [profile]
|
(validate-profile [profile]
|
||||||
(when-not profile
|
(when-not profile
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code ::wrong-credentials))
|
:code ::wrong-credentials))
|
||||||
|
@ -80,14 +169,51 @@
|
||||||
profile)]
|
profile)]
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
(let [prof (-> (retrieve-profile-by-email conn email)
|
(let [prof (-> (retrieve-profile-by-email conn email)
|
||||||
(check-profile)
|
(validate-profile)
|
||||||
(profile/strip-private-attrs))
|
(profile/strip-private-attrs))
|
||||||
addt (profile/retrieve-additional-data conn (:id prof))]
|
addt (profile/retrieve-additional-data conn (:id prof))]
|
||||||
(merge prof addt)))))
|
(merge prof addt)))))
|
||||||
|
|
||||||
|
(def sql:profile-by-email
|
||||||
|
"select * from profile
|
||||||
|
where email=? and deleted_at is null
|
||||||
|
for update")
|
||||||
|
|
||||||
(defn- retrieve-profile-by-email
|
(defn- retrieve-profile-by-email
|
||||||
[conn email]
|
[conn email]
|
||||||
(db/get-by-params conn :profile {:email email} {:for-update true}))
|
(let [email (str/lower email)]
|
||||||
|
(db/exec-one! conn [sql:profile-by-email email])))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Mutation: Register if not exists
|
||||||
|
|
||||||
|
(sm/defmutation ::login-or-register
|
||||||
|
[{:keys [email fullname] :as params}]
|
||||||
|
(letfn [(populate-additional-data [conn profile]
|
||||||
|
(let [data (profile/retrieve-additional-data conn (:id profile))]
|
||||||
|
(merge profile data)))
|
||||||
|
|
||||||
|
(create-profile [conn {:keys [fullname email]}]
|
||||||
|
(db/insert! conn :profile
|
||||||
|
{:id (uuid/next)
|
||||||
|
:fullname fullname
|
||||||
|
:email (str/lower email)
|
||||||
|
:pending-email nil
|
||||||
|
:photo ""
|
||||||
|
:password "!"
|
||||||
|
:is-demo false}))
|
||||||
|
|
||||||
|
(register-profile [conn params]
|
||||||
|
(->> (create-profile conn params)
|
||||||
|
(create-profile-relations conn)))]
|
||||||
|
|
||||||
|
(db/with-atomic [conn db/pool]
|
||||||
|
(let [profile (retrieve-profile-by-email conn email)
|
||||||
|
profile (if profile
|
||||||
|
(populate-additional-data conn profile)
|
||||||
|
(register-profile conn params))]
|
||||||
|
(profile/strip-private-attrs profile)))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Update Profile (own)
|
;; --- Mutation: Update Profile (own)
|
||||||
|
@ -182,108 +308,6 @@
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Register Profile
|
|
||||||
|
|
||||||
(declare check-profile-existence!)
|
|
||||||
(declare register-profile)
|
|
||||||
|
|
||||||
(s/def ::register-profile
|
|
||||||
(s/keys :req-un [::email ::password ::fullname]))
|
|
||||||
|
|
||||||
(defn email-domain-in-whitelist?
|
|
||||||
"Returns true if email's domain is in the given whitelist or if given
|
|
||||||
whitelist is an empty string."
|
|
||||||
[whitelist email]
|
|
||||||
(if (str/blank? whitelist)
|
|
||||||
true
|
|
||||||
(let [domains (str/split whitelist #",\s*")
|
|
||||||
email-domain (second (str/split email #"@"))]
|
|
||||||
(contains? (set domains) email-domain))))
|
|
||||||
|
|
||||||
(sm/defmutation ::register-profile
|
|
||||||
[params]
|
|
||||||
(when-not (:registration-enabled cfg/config)
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code ::registration-disabled))
|
|
||||||
|
|
||||||
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
|
|
||||||
(:email params))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code ::email-domain-is-not-allowed))
|
|
||||||
|
|
||||||
(db/with-atomic [conn db/pool]
|
|
||||||
(check-profile-existence! conn params)
|
|
||||||
(let [profile (register-profile conn params)
|
|
||||||
token (-> (sodi.prng/random-bytes 32)
|
|
||||||
(sodi.util/bytes->b64s))
|
|
||||||
payload {:type :verify-email
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:email (:email profile)}]
|
|
||||||
|
|
||||||
(db/insert! conn :generic-token
|
|
||||||
{:token token
|
|
||||||
:valid-until (dt/plus (dt/now)
|
|
||||||
(dt/duration {:days 30}))
|
|
||||||
:content (blob/encode payload)})
|
|
||||||
|
|
||||||
(emails/send! conn emails/register
|
|
||||||
{:to (:email profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:public-url (:public-url cfg/config)
|
|
||||||
:token token})
|
|
||||||
profile)))
|
|
||||||
|
|
||||||
(def ^:private sql:profile-existence
|
|
||||||
"select exists (select * from profile
|
|
||||||
where email = ?
|
|
||||||
and deleted_at is null) as val")
|
|
||||||
|
|
||||||
(defn- check-profile-existence!
|
|
||||||
[conn {:keys [email] :as params}]
|
|
||||||
(let [email (str/lower email)
|
|
||||||
result (db/exec-one! conn [sql:profile-existence email])]
|
|
||||||
(when (:val result)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code ::email-already-exists))
|
|
||||||
params))
|
|
||||||
|
|
||||||
(defn- create-profile
|
|
||||||
"Create the profile entry on the database with limited input
|
|
||||||
filling all the other fields with defaults."
|
|
||||||
[conn {:keys [id fullname email password demo?] :as params}]
|
|
||||||
(let [id (or id (uuid/next))
|
|
||||||
demo? (if (boolean? demo?) demo? false)
|
|
||||||
password (sodi.pwhash/derive password)]
|
|
||||||
(db/insert! conn :profile
|
|
||||||
{:id id
|
|
||||||
:fullname fullname
|
|
||||||
:email (str/lower email)
|
|
||||||
:pending-email (if demo? nil email)
|
|
||||||
:photo ""
|
|
||||||
:password password
|
|
||||||
:is-demo demo?})))
|
|
||||||
|
|
||||||
(defn register-profile
|
|
||||||
[conn params]
|
|
||||||
(let [prof (create-profile conn params)
|
|
||||||
team (mt.teams/create-team conn {:profile-id (:id prof)
|
|
||||||
:name "Default"
|
|
||||||
:default? true})
|
|
||||||
proj (mt.projects/create-project conn {:profile-id (:id prof)
|
|
||||||
:team-id (:id team)
|
|
||||||
:name "Drafts"
|
|
||||||
:default? true})]
|
|
||||||
(mt.teams/create-team-profile conn {:team-id (:id team)
|
|
||||||
:profile-id (:id prof)})
|
|
||||||
(mt.projects/create-project-profile conn {:project-id (:id proj)
|
|
||||||
:profile-id (:id prof)})
|
|
||||||
|
|
||||||
;; TODO: rename to -default-team-id...
|
|
||||||
(merge (profile/strip-private-attrs prof)
|
|
||||||
{:default-team (:id team)
|
|
||||||
:default-project (:id proj)})))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Request Email Change
|
;; --- Mutation: Request Email Change
|
||||||
|
|
||||||
(declare select-profile-for-update)
|
(declare select-profile-for-update)
|
||||||
|
@ -296,11 +320,11 @@
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
(let [email (str/lower email)
|
(let [email (str/lower email)
|
||||||
profile (select-profile-for-update conn profile-id)
|
profile (select-profile-for-update conn profile-id)
|
||||||
token (-> (sodi.prng/random-bytes 32)
|
|
||||||
(sodi.util/bytes->b64s))
|
|
||||||
payload {:type :change-email
|
payload {:type :change-email
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:email email}]
|
:email email}
|
||||||
|
|
||||||
|
token (tokens/create! conn payload)]
|
||||||
|
|
||||||
(when (not= email (:email profile))
|
(when (not= email (:email profile))
|
||||||
(check-profile-existence! conn params))
|
(check-profile-existence! conn params))
|
||||||
|
@ -309,21 +333,14 @@
|
||||||
{:pending-email email}
|
{:pending-email email}
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(db/insert! conn :generic-token
|
|
||||||
{:token token
|
|
||||||
:valid-until (dt/plus (dt/now)
|
|
||||||
(dt/duration {:hours 48}))
|
|
||||||
:content (blob/encode payload)})
|
|
||||||
|
|
||||||
(emails/send! conn emails/change-email
|
(emails/send! conn emails/change-email
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
:public-url (:public-url cfg/config)
|
:public-url (:public-uri cfg/config)
|
||||||
:pending-email email
|
:pending-email email
|
||||||
:token token})
|
:token token})
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
|
||||||
(defn- select-profile-for-update
|
(defn- select-profile-for-update
|
||||||
[conn id]
|
[conn id]
|
||||||
(db/get-by-id conn :profile id {:for-update true}))
|
(db/get-by-id conn :profile id {:for-update true}))
|
||||||
|
@ -334,16 +351,14 @@
|
||||||
;; Generic mutation for perform token based verification for auth
|
;; Generic mutation for perform token based verification for auth
|
||||||
;; domain.
|
;; domain.
|
||||||
|
|
||||||
(declare retrieve-token)
|
|
||||||
|
|
||||||
(s/def ::verify-profile-token
|
(s/def ::verify-profile-token
|
||||||
(s/keys :req-un [::token]))
|
(s/keys :req-un [::token]))
|
||||||
|
|
||||||
(sm/defmutation ::verify-profile-token
|
(sm/defmutation ::verify-profile-token
|
||||||
[{:keys [token] :as params}]
|
[{:keys [token] :as params}]
|
||||||
(letfn [(handle-email-change [conn token]
|
(letfn [(handle-email-change [conn tdata]
|
||||||
(let [profile (select-profile-for-update conn (:profile-id token))]
|
(let [profile (select-profile-for-update conn (:profile-id tdata))]
|
||||||
(when (not= (:email token)
|
(when (not= (:email tdata)
|
||||||
(:pending-email profile))
|
(:pending-email profile))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code ::email-does-not-match))
|
:code ::email-does-not-match))
|
||||||
|
@ -353,48 +368,31 @@
|
||||||
:email (:pending-email profile)}
|
:email (:pending-email profile)}
|
||||||
{:id (:id profile)})
|
{:id (:id profile)})
|
||||||
|
|
||||||
token))
|
tdata))
|
||||||
|
|
||||||
(handle-email-verify [conn token]
|
(handle-email-verify [conn tdata]
|
||||||
(let [profile (select-profile-for-update conn (:profile-id token))]
|
(let [profile (select-profile-for-update conn (:profile-id tdata))]
|
||||||
(when (or (not= (:email profile)
|
(when (or (not= (:email profile)
|
||||||
(:pending-email profile))
|
(:pending-email profile))
|
||||||
(not= (:email profile)
|
(not= (:email profile)
|
||||||
(:email token)))
|
(:email tdata)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code ::invalid-token))
|
:code ::tokens/invalid-token))
|
||||||
|
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
{:pending-email nil}
|
{:pending-email nil}
|
||||||
{:id (:id profile)})
|
{:id (:id profile)})
|
||||||
token))]
|
tdata))]
|
||||||
|
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
(let [token (retrieve-token conn token)]
|
(let [tdata (tokens/retrieve conn token {:delete true})]
|
||||||
(db/delete! conn :generic-token {:token (:token params)})
|
(tokens/delete! conn token)
|
||||||
|
(case (:type tdata)
|
||||||
;; Validate the token expiration
|
:change-email (handle-email-change conn tdata)
|
||||||
(when (> (inst-ms (dt/now))
|
:verify-email (handle-email-verify conn tdata)
|
||||||
(inst-ms (:valid-until token)))
|
:authentication tdata
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code ::invalid-token))
|
:code ::tokens/invalid-token))))))
|
||||||
|
|
||||||
(case (:type token)
|
|
||||||
:change-email (handle-email-change conn token)
|
|
||||||
:verify-email (handle-email-verify conn token)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code ::invalid-token))))))
|
|
||||||
|
|
||||||
(defn- retrieve-token
|
|
||||||
[conn token]
|
|
||||||
(let [row (-> (db/get-by-params conn :generic-token {:token token})
|
|
||||||
(decode-token-row))]
|
|
||||||
(when-not row
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code ::invalid-token))
|
|
||||||
(-> row
|
|
||||||
(dissoc :content)
|
|
||||||
(merge (:content row)))))
|
|
||||||
|
|
||||||
;; --- Mutation: Cancel Email Change
|
;; --- Mutation: Cancel Email Change
|
||||||
|
|
||||||
|
@ -421,20 +419,15 @@
|
||||||
(sm/defmutation ::request-profile-recovery
|
(sm/defmutation ::request-profile-recovery
|
||||||
[{:keys [email] :as params}]
|
[{:keys [email] :as params}]
|
||||||
(letfn [(create-recovery-token [conn {:keys [id] :as profile}]
|
(letfn [(create-recovery-token [conn {:keys [id] :as profile}]
|
||||||
(let [token (-> (sodi.prng/random-bytes 32)
|
(let [payload {:type :password-recovery-token
|
||||||
(sodi.util/bytes->b64s))
|
:profile-id id}
|
||||||
payload {:type :password-recovery-token
|
token (tokens/create! conn payload)]
|
||||||
:profile-id id}]
|
|
||||||
(db/insert! conn :generic-token
|
|
||||||
{:token token
|
|
||||||
:valid-until (dt/plus (dt/now) (dt/duration {:hours 24}))
|
|
||||||
:content (blob/encode payload)})
|
|
||||||
(assoc profile :token token)))
|
(assoc profile :token token)))
|
||||||
|
|
||||||
(send-email-notification [conn profile]
|
(send-email-notification [conn profile]
|
||||||
(emails/send! conn emails/password-recovery
|
(emails/send! conn emails/password-recovery
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:public-url (:public-url cfg/config)
|
:public-url (:public-uri cfg/config)
|
||||||
:token (:token profile)
|
:token (:token profile)
|
||||||
:name (:fullname profile)}))]
|
:name (:fullname profile)}))]
|
||||||
|
|
||||||
|
@ -454,13 +447,11 @@
|
||||||
(sm/defmutation ::recover-profile
|
(sm/defmutation ::recover-profile
|
||||||
[{:keys [token password]}]
|
[{:keys [token password]}]
|
||||||
(letfn [(validate-token [conn token]
|
(letfn [(validate-token [conn token]
|
||||||
(let [{:keys [token content]}
|
(let [tpayload (tokens/retrieve conn token)]
|
||||||
(-> (db/get-by-params conn :generic-token {:token token})
|
(when (not= (:type tpayload) :password-recovery-token)
|
||||||
(decode-token-row))]
|
|
||||||
(when (not= (:type content) :password-recovery-token)
|
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-token))
|
:code ::tokens/invalid-token))
|
||||||
(:profile-id content)))
|
(:profile-id tpayload)))
|
||||||
|
|
||||||
(update-password [conn profile-id]
|
(update-password [conn profile-id]
|
||||||
(let [pwd (sodi.pwhash/derive password)]
|
(let [pwd (sodi.pwhash/derive password)]
|
||||||
|
|
|
@ -91,5 +91,5 @@
|
||||||
|
|
||||||
(defn strip-private-attrs
|
(defn strip-private-attrs
|
||||||
"Only selects a publicy visible profile attrs."
|
"Only selects a publicy visible profile attrs."
|
||||||
[o]
|
[row]
|
||||||
(dissoc o :password :deleted-at))
|
(dissoc row :password :deleted-at))
|
||||||
|
|
80
backend/src/uxbox/services/tokens.clj
Normal file
80
backend/src/uxbox/services/tokens.clj
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns uxbox.services.tokens
|
||||||
|
(:refer-clojure :exclude [next])
|
||||||
|
(:require
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[sodi.prng]
|
||||||
|
[sodi.util]
|
||||||
|
[uxbox.common.exceptions :as ex]
|
||||||
|
[uxbox.common.spec :as us]
|
||||||
|
[uxbox.common.uuid :as uuid]
|
||||||
|
[uxbox.config :as cfg]
|
||||||
|
[uxbox.util.time :as dt]
|
||||||
|
[uxbox.util.blob :as blob]
|
||||||
|
[uxbox.db :as db]))
|
||||||
|
|
||||||
|
(defn next
|
||||||
|
([] (next 64))
|
||||||
|
([n]
|
||||||
|
(-> (sodi.prng/random-bytes n)
|
||||||
|
(sodi.util/bytes->b64s))))
|
||||||
|
|
||||||
|
(def default-duration
|
||||||
|
(dt/duration {:hours 48}))
|
||||||
|
|
||||||
|
(defn- decode-row
|
||||||
|
[{:keys [content] :as row}]
|
||||||
|
(when row
|
||||||
|
(cond-> row
|
||||||
|
content (assoc :content (blob/decode content)))))
|
||||||
|
|
||||||
|
(defn create!
|
||||||
|
([conn payload] (create! conn payload {}))
|
||||||
|
([conn payload {:keys [valid] :or {valid default-duration}}]
|
||||||
|
(let [token (next)
|
||||||
|
until (dt/plus (dt/now) (dt/duration valid))]
|
||||||
|
(db/insert! conn :generic-token
|
||||||
|
{:content (blob/encode payload)
|
||||||
|
:token token
|
||||||
|
:valid-until until})
|
||||||
|
token)))
|
||||||
|
|
||||||
|
(defn delete!
|
||||||
|
[conn token]
|
||||||
|
(db/delete! conn :generic-token {:token token}))
|
||||||
|
|
||||||
|
(defn retrieve
|
||||||
|
([conn token] (retrieve conn token {}))
|
||||||
|
([conn token {:keys [delete] :or {delete false}}]
|
||||||
|
(let [row (->> (db/query conn :generic-token {:token token})
|
||||||
|
(map decode-row)
|
||||||
|
(first))]
|
||||||
|
|
||||||
|
(when-not row
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code ::invalid-token))
|
||||||
|
|
||||||
|
;; Validate the token expiration
|
||||||
|
(when (> (inst-ms (dt/now))
|
||||||
|
(inst-ms (:valid-until row)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code ::invalid-token))
|
||||||
|
|
||||||
|
(when delete
|
||||||
|
(db/delete! conn :generic-token {:token token}))
|
||||||
|
|
||||||
|
(-> row
|
||||||
|
(dissoc :content)
|
||||||
|
(merge (:content row))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -82,10 +82,12 @@
|
||||||
|
|
||||||
(defn create-profile
|
(defn create-profile
|
||||||
[conn i]
|
[conn i]
|
||||||
(#'profile/register-profile conn {:id (mk-uuid "profile" i)
|
(let [params {:id (mk-uuid "profile" i)
|
||||||
:fullname (str "Profile " i)
|
:fullname (str "Profile " i)
|
||||||
:email (str "profile" i ".test@nodomain.com")
|
:email (str "profile" i ".test@nodomain.com")
|
||||||
:password "123123"}))
|
:password "123123"}]
|
||||||
|
(->> (#'profile/create-profile conn params)
|
||||||
|
(#'profile/create-profile-relations conn))))
|
||||||
|
|
||||||
(defn create-team
|
(defn create-team
|
||||||
[conn profile-id i]
|
[conn profile-id i]
|
||||||
|
|
|
@ -52,4 +52,9 @@
|
||||||
.form-container {
|
.form-container {
|
||||||
width: 368px;
|
width: 368px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-google-auth {
|
||||||
|
margin-bottom: $medium;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
;; defined by the Mozilla Public License, v. 2.0.
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.main.data.auth
|
(ns uxbox.main.data.auth
|
||||||
(:require
|
(:require
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
(watch [this state stream]
|
(watch [this state stream]
|
||||||
(let [team-id (:default-team-id data)]
|
(let [team-id (:default-team-id data)]
|
||||||
(rx/of (du/profile-fetched data)
|
(rx/of (du/profile-fetched data)
|
||||||
(rt/navigate :dashboard-team {:team-id team-id}))))))
|
(rt/nav :dashboard-team {:team-id team-id}))))))
|
||||||
|
|
||||||
;; --- Login
|
;; --- Login
|
||||||
|
|
||||||
|
@ -63,6 +63,20 @@
|
||||||
(on-error err)
|
(on-error err)
|
||||||
(rx/empty)))
|
(rx/empty)))
|
||||||
(rx/map logged-in))))))
|
(rx/map logged-in))))))
|
||||||
|
|
||||||
|
(defn login-from-token
|
||||||
|
[{:keys [profile] :as tdata}]
|
||||||
|
(ptk/reify ::login-from-token
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(merge state (dissoc initial-state :route :router)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [this state s]
|
||||||
|
(let [team-id (:default-team-id profile)]
|
||||||
|
(rx/of (du/profile-fetched profile)
|
||||||
|
(rt/nav' :dashboard-team {:team-id team-id}))))))
|
||||||
|
|
||||||
;; --- Logout
|
;; --- Logout
|
||||||
|
|
||||||
(def clear-user-data
|
(def clear-user-data
|
||||||
|
|
|
@ -62,6 +62,12 @@
|
||||||
([id] (mutation id {}))
|
([id] (mutation id {}))
|
||||||
([id params] (mutation id params)))
|
([id params] (mutation id params)))
|
||||||
|
|
||||||
|
(defmethod mutation :login-with-google
|
||||||
|
[id params]
|
||||||
|
(let [url (str url "/api/oauth/google")]
|
||||||
|
(->> (http/send! {:method :post :url url})
|
||||||
|
(rx/mapcat handle-response))))
|
||||||
|
|
||||||
(defmethod mutation :upload-image
|
(defmethod mutation :upload-image
|
||||||
[id params]
|
[id params]
|
||||||
(let [form (js/FormData.)]
|
(let [form (js/FormData.)]
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[uxbox.main.ui.icons :as i]
|
[uxbox.main.ui.icons :as i]
|
||||||
|
[uxbox.main.data.auth :as da]
|
||||||
[uxbox.main.data.users :as du]
|
[uxbox.main.data.users :as du]
|
||||||
[uxbox.main.data.messages :as dm]
|
[uxbox.main.data.messages :as dm]
|
||||||
[uxbox.main.store :as st]
|
[uxbox.main.store :as st]
|
||||||
|
@ -66,6 +67,10 @@
|
||||||
(st/emit! (rt/nav :settings-profile)
|
(st/emit! (rt/nav :settings-profile)
|
||||||
du/fetch-profile)))
|
du/fetch-profile)))
|
||||||
|
|
||||||
|
(defn- handle-authentication
|
||||||
|
[tdata]
|
||||||
|
(st/emit! (da/login-from-token tdata)))
|
||||||
|
|
||||||
(mf/defc verify-token
|
(mf/defc verify-token
|
||||||
[{:keys [route] :as props}]
|
[{:keys [route] :as props}]
|
||||||
(let [token (get-in route [:query-params :token])]
|
(let [token (get-in route [:query-params :token])]
|
||||||
|
@ -73,10 +78,11 @@
|
||||||
(fn []
|
(fn []
|
||||||
(->> (rp/mutation :verify-profile-token {:token token})
|
(->> (rp/mutation :verify-profile-token {:token token})
|
||||||
(rx/subs
|
(rx/subs
|
||||||
(fn [response]
|
(fn [tdata]
|
||||||
(case (:type response)
|
(case (:type tdata)
|
||||||
:verify-email (handle-email-verified response)
|
:verify-email (handle-email-verified tdata)
|
||||||
:change-email (handle-email-changed response)
|
:change-email (handle-email-changed tdata)
|
||||||
|
:authentication (handle-authentication tdata)
|
||||||
nil))
|
nil))
|
||||||
(fn [error]
|
(fn [error]
|
||||||
(case (:code error)
|
(case (:code error)
|
||||||
|
|
|
@ -10,13 +10,16 @@
|
||||||
(ns uxbox.main.ui.auth.login
|
(ns uxbox.main.ui.auth.login
|
||||||
(:require
|
(:require
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
|
[beicon.core :as rx]
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
[uxbox.common.spec :as us]
|
[uxbox.common.spec :as us]
|
||||||
[uxbox.main.ui.icons :as i]
|
[uxbox.main.ui.icons :as i]
|
||||||
[uxbox.main.data.auth :as da]
|
[uxbox.main.data.auth :as da]
|
||||||
|
[uxbox.main.repo :as rp]
|
||||||
[uxbox.main.store :as st]
|
[uxbox.main.store :as st]
|
||||||
[uxbox.main.data.messages :as dm]
|
[uxbox.main.data.messages :as dm]
|
||||||
[uxbox.main.ui.components.forms :refer [input submit-button form]]
|
[uxbox.main.ui.components.forms :refer [input submit-button form]]
|
||||||
|
[uxbox.util.object :as obj]
|
||||||
[uxbox.util.dom :as dom]
|
[uxbox.util.dom :as dom]
|
||||||
[uxbox.util.forms :as fm]
|
[uxbox.util.forms :as fm]
|
||||||
[uxbox.util.i18n :refer [tr t]]
|
[uxbox.util.i18n :refer [tr t]]
|
||||||
|
@ -38,6 +41,13 @@
|
||||||
{:on-error (partial on-error form)})]
|
{:on-error (partial on-error form)})]
|
||||||
(st/emit! (da/login params))))
|
(st/emit! (da/login params))))
|
||||||
|
|
||||||
|
(defn- login-with-google
|
||||||
|
[event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(->> (rp/mutation! :login-with-google {})
|
||||||
|
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
||||||
|
(.replace js/location redirect-uri)))))
|
||||||
|
|
||||||
(mf/defc login-form
|
(mf/defc login-form
|
||||||
[{:keys [locale] :as props}]
|
[{:keys [locale] :as props}]
|
||||||
[:& form {:on-submit on-submit
|
[:& form {:on-submit on-submit
|
||||||
|
@ -60,6 +70,7 @@
|
||||||
|
|
||||||
(mf/defc login-page
|
(mf/defc login-page
|
||||||
[{:keys [locale] :as props}]
|
[{:keys [locale] :as props}]
|
||||||
|
|
||||||
[:div.generic-form.login-form
|
[:div.generic-form.login-form
|
||||||
[:div.form-container
|
[:div.form-container
|
||||||
[:h1 (t locale "auth.login-title")]
|
[:h1 (t locale "auth.login-title")]
|
||||||
|
@ -67,6 +78,10 @@
|
||||||
|
|
||||||
[:& login-form {:locale locale}]
|
[:& login-form {:locale locale}]
|
||||||
|
|
||||||
|
[:a.btn-secondary.btn-large.btn-google-auth
|
||||||
|
{:on-click login-with-google}
|
||||||
|
"Login with google"]
|
||||||
|
|
||||||
[:div.links
|
[:div.links
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
|
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
;; 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/.
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
;;
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
;; Copyright (c) 2020 UXBOX Labs SL
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
(ns uxbox.main.ui.settings.header
|
(ns uxbox.main.ui.settings.header
|
||||||
|
@ -55,21 +58,3 @@
|
||||||
{:class "foobar"
|
{:class "foobar"
|
||||||
:on-click #(st/emit! (rt/nav :settings-profile))}
|
:on-click #(st/emit! (rt/nav :settings-profile))}
|
||||||
(t locale "settings.teams")]]]))
|
(t locale "settings.teams")]]]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;; [:div.main-logo
|
|
||||||
;; {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
|
|
||||||
;; i/logo-icon]
|
|
||||||
;; [:section.main-bar
|
|
||||||
;; [:nav
|
|
||||||
;; [:a.nav-item
|
|
||||||
;; {:class (when profile? "current")
|
|
||||||
;; :on-click #(st/emit! (rt/nav :settings-profile))}
|
|
||||||
;; (t locale "settings.profile")]
|
|
||||||
;; [:a.nav-item
|
|
||||||
;; {:class (when password? "current")
|
|
||||||
;; :on-click #(st/emit! (rt/nav :settings-password))}
|
|
||||||
;; (t locale "settings.password")]]]]))
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue