mirror of
https://github.com/penpot/penpot.git
synced 2025-06-01 23:01:40 +02:00
♻️ Initial profile and auth refactor.
This commit is contained in:
parent
d0defe5d93
commit
7d5f9c1078
59 changed files with 2712 additions and 1407 deletions
|
@ -26,6 +26,8 @@
|
|||
:database-username "uxbox"
|
||||
:database-password "uxbox"
|
||||
|
||||
:public-url "http://localhost:3449"
|
||||
|
||||
:redis-uri "redis://redis/0"
|
||||
:media-directory "resources/public/media"
|
||||
:assets-directory "resources/public/static"
|
||||
|
@ -67,11 +69,13 @@
|
|||
(s/def ::registration-enabled ::us/boolean)
|
||||
(s/def ::registration-domain-whitelist ::us/string)
|
||||
(s/def ::debug-humanize-transit ::us/boolean)
|
||||
(s/def ::public-url ::us/string)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::http-server-cors
|
||||
::http-server-debug
|
||||
::http-server-port
|
||||
::public-url
|
||||
::database-username
|
||||
::database-password
|
||||
::database-uri
|
||||
|
|
|
@ -62,3 +62,11 @@
|
|||
(def password-recovery
|
||||
"A password recovery notification email."
|
||||
(emails/build ::password-recovery default-context))
|
||||
|
||||
(s/def ::pending-email ::us/string)
|
||||
(s/def ::change-email
|
||||
(s/keys :req-un [::name ::pending-email ::token]))
|
||||
|
||||
(def change-email
|
||||
"Password change confirmation email"
|
||||
(emails/build ::change-email default-context))
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#{:create-demo-profile
|
||||
:logout
|
||||
:profile
|
||||
:verify-profile-token
|
||||
:recover-profile
|
||||
:register-profile
|
||||
:request-profile-recovery
|
||||
|
@ -50,8 +51,17 @@
|
|||
(:profile-id req) (assoc :profile-id (:profile-id req)))]
|
||||
(if (or (:profile-id req)
|
||||
(contains? unauthorized-services type))
|
||||
{:status 200
|
||||
:body (sm/handle (with-meta data {:req req}))}
|
||||
(let [body (sm/handle (with-meta data {:req req}))]
|
||||
(if (= type :delete-profile)
|
||||
(do
|
||||
(some-> (get-in req [:cookies "auth-token" :value])
|
||||
(uuid/uuid)
|
||||
(session/delete))
|
||||
{:status 204
|
||||
:cookies {"auth-token" {:value "" :max-age -1}}
|
||||
:body ""})
|
||||
{:status 200
|
||||
:body body}))
|
||||
{:status 403
|
||||
:body {:type :authentication
|
||||
:code :unauthorized}})))
|
||||
|
@ -68,11 +78,11 @@
|
|||
|
||||
(defn logout-handler
|
||||
[req]
|
||||
(some-> (get-in req [:cookies "auth-token"])
|
||||
(some-> (get-in req [:cookies "auth-token" :value])
|
||||
(uuid/uuid)
|
||||
(session/delete))
|
||||
{:status 204
|
||||
:cookies {"auth-token" nil}
|
||||
{:status 200
|
||||
:cookies {"auth-token" {:value "" :max-age -1}}
|
||||
:body ""})
|
||||
|
||||
(defn echo-handler
|
||||
|
|
|
@ -34,8 +34,11 @@
|
|||
:name "0006-presence"
|
||||
:fn (mg/resource "migrations/0006.presence.sql")}
|
||||
{:desc "Remove version"
|
||||
:name "0007.remove_version"
|
||||
:fn (mg/resource "migrations/0007.remove_version.sql")}]})
|
||||
:name "0007-remove-version"
|
||||
:fn (mg/resource "migrations/0007.remove-version.sql")}]})
|
||||
{:desc "Initial generic token tables"
|
||||
:name "0008-generic-token"
|
||||
:fn (mg/resource "migrations/0007.generic-token.sql")}]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Entry point
|
||||
|
|
|
@ -46,6 +46,12 @@
|
|||
(s/def ::old-password ::us/string)
|
||||
(s/def ::theme ::us/string)
|
||||
|
||||
(defn decode-token-row
|
||||
[{:keys [content] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
content (assoc :content (blob/decode content)))))
|
||||
|
||||
|
||||
;; --- Mutation: Login
|
||||
|
||||
|
@ -86,15 +92,6 @@
|
|||
|
||||
;; --- Mutation: Update Profile (own)
|
||||
|
||||
(def ^:private sql:update-profile
|
||||
"update profile
|
||||
set fullname = $2,
|
||||
lang = $3,
|
||||
theme = $4
|
||||
where id = $1
|
||||
and deleted_at is null
|
||||
returning *")
|
||||
|
||||
(defn- update-profile
|
||||
[conn {:keys [id fullname lang theme] :as params}]
|
||||
(db/update! conn :profile
|
||||
|
@ -117,7 +114,7 @@
|
|||
|
||||
(defn- validate-password!
|
||||
[conn {:keys [profile-id old-password] :as params}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
(let [profile (profile/retrieve-profile-data conn profile-id)
|
||||
result (sodi.pwhash/verify old-password (:password profile))]
|
||||
(when-not (:valid result)
|
||||
(ex/raise :type :validation
|
||||
|
@ -179,14 +176,10 @@
|
|||
|
||||
(defn- update-profile-photo
|
||||
[conn profile-id path]
|
||||
(let [sql "update profile set photo=$1
|
||||
where id=$2
|
||||
and deleted_at is null
|
||||
returning id"]
|
||||
(db/update! conn :profile
|
||||
{:photo (str path)}
|
||||
{:id profile-id})
|
||||
nil))
|
||||
(db/update! conn :profile
|
||||
{:photo (str path)}
|
||||
{:id profile-id})
|
||||
nil)
|
||||
|
||||
|
||||
;; --- Mutation: Register Profile
|
||||
|
@ -211,36 +204,44 @@
|
|||
[params]
|
||||
(when-not (:registration-enabled cfg/config)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
: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)]
|
||||
;; TODO: send a correct link for email verification
|
||||
(let [data {:to (:email params)
|
||||
:name (:fullname params)}]
|
||||
(emails/send! conn emails/register data)
|
||||
profile))))
|
||||
(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)}]
|
||||
|
||||
(def ^:private sql:insert-profile
|
||||
"insert into profile (id, fullname, email, password, photo, is_demo)
|
||||
values ($1, $2, $3, $4, '', $5) returning *")
|
||||
(db/insert! conn :generic-token
|
||||
{:token token
|
||||
:valid-until (dt/plus (dt/now)
|
||||
(dt/duration {:days 30}))
|
||||
:content (blob/encode payload)})
|
||||
|
||||
(def ^:private sql:insert-email
|
||||
"insert into profile_email (profile_id, email, is_main)
|
||||
values ($1, $2, true)")
|
||||
(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 = $1
|
||||
where email = ?
|
||||
and deleted_at is null) as val")
|
||||
|
||||
(defn- check-profile-existence!
|
||||
[conn {:keys [email] :as params}]
|
||||
(let [result (db/exec-one! conn [sql:profile-existence email])]
|
||||
(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))
|
||||
|
@ -256,68 +257,192 @@
|
|||
(db/insert! conn :profile
|
||||
{:id id
|
||||
:fullname fullname
|
||||
:email email
|
||||
:email (str/lower email)
|
||||
:pending-email (if demo? nil email)
|
||||
:photo ""
|
||||
:password password
|
||||
:is-demo demo?})))
|
||||
|
||||
(defn- create-profile-email
|
||||
[conn {:keys [id email] :as profile}]
|
||||
(db/insert! conn :profile-email
|
||||
{:profile-id id
|
||||
:email email
|
||||
:is-main true}))
|
||||
|
||||
(defn register-profile
|
||||
[conn params]
|
||||
(let [prof (create-profile conn params)
|
||||
_ (create-profile-email conn prof)
|
||||
|
||||
team (mt.teams/create-team conn {:profile-id (:id prof)
|
||||
:name "Default"
|
||||
:default? true})
|
||||
_ (mt.teams/create-team-profile conn {:team-id (:id team)
|
||||
:profile-id (:id prof)})
|
||||
|
||||
proj (mt.projects/create-project conn {:profile-id (:id prof)
|
||||
:team-id (:id team)
|
||||
:name "Drafts"
|
||||
:default? true})
|
||||
_ (mt.projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id (:id prof)})
|
||||
]
|
||||
: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
|
||||
|
||||
(declare select-profile-for-update)
|
||||
|
||||
(s/def ::request-email-change
|
||||
(s/keys :req-un [::email]))
|
||||
|
||||
(sm/defmutation ::request-email-change
|
||||
[{:keys [profile-id email] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [email (str/lower email)
|
||||
profile (select-profile-for-update conn profile-id)
|
||||
token (-> (sodi.prng/random-bytes 32)
|
||||
(sodi.util/bytes->b64s))
|
||||
payload {:type :change-email
|
||||
:profile-id profile-id
|
||||
:email email}]
|
||||
|
||||
(when (not= email (:email profile))
|
||||
(check-profile-existence! conn params))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:pending-email email}
|
||||
{: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
|
||||
{:to (:email profile)
|
||||
:name (:fullname profile)
|
||||
:public-url (:public-url cfg/config)
|
||||
:pending-email email
|
||||
:token token})
|
||||
nil)))
|
||||
|
||||
|
||||
(defn- select-profile-for-update
|
||||
[conn id]
|
||||
(db/get-by-id conn :profile id {:for-update true}))
|
||||
|
||||
|
||||
;; --- Mutation: Verify Profile Token
|
||||
|
||||
;; Generic mutation for perform token based verification for auth
|
||||
;; domain.
|
||||
|
||||
(declare retrieve-token)
|
||||
|
||||
(s/def ::verify-profile-token
|
||||
(s/keys :req-un [::token]))
|
||||
|
||||
(sm/defmutation ::verify-profile-token
|
||||
[{:keys [token] :as params}]
|
||||
(letfn [(handle-email-change [conn token]
|
||||
(let [profile (select-profile-for-update conn (:profile-id token))]
|
||||
(when (not= (:email token)
|
||||
(:pending-email profile))
|
||||
(ex/raise :type :validation
|
||||
:code ::email-does-not-match))
|
||||
(check-profile-existence! conn {:email (:pending-email profile)})
|
||||
(db/update! conn :profile
|
||||
{:pending-email nil
|
||||
:email (:pending-email profile)}
|
||||
{:id (:id profile)})
|
||||
|
||||
token))
|
||||
|
||||
(handle-email-verify [conn token]
|
||||
(let [profile (select-profile-for-update conn (:profile-id token))]
|
||||
(when (or (not= (:email profile)
|
||||
(:pending-email profile))
|
||||
(not= (:email profile)
|
||||
(:email token)))
|
||||
(ex/raise :type :validation
|
||||
:code ::invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:pending-email nil}
|
||||
{:id (:id profile)})
|
||||
token))]
|
||||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [token (retrieve-token conn token)]
|
||||
(db/delete! conn :generic-token {:token (:token params)})
|
||||
|
||||
;; Validate the token expiration
|
||||
(when (> (inst-ms (dt/now))
|
||||
(inst-ms (:valid-until token)))
|
||||
(ex/raise :type :validation
|
||||
:code ::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
|
||||
|
||||
(s/def ::cancel-email-change
|
||||
(s/keys :req-un [::profile-id]))
|
||||
|
||||
(sm/defmutation ::cancel-email-change
|
||||
[{:keys [profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [profile (select-profile-for-update conn profile-id)]
|
||||
(when (= (:email profile)
|
||||
(:pending-email profile))
|
||||
(ex/raise :type :validation
|
||||
:code ::unexpected-request))
|
||||
|
||||
(db/update! conn :profile {:pending-email nil} {:id profile-id})
|
||||
nil)))
|
||||
|
||||
;; --- Mutation: Request Profile Recovery
|
||||
|
||||
(s/def ::request-profile-recovery
|
||||
(s/keys :req-un [::email]))
|
||||
|
||||
(def sql:insert-recovery-token
|
||||
"insert into password_recovery_token (profile_id, token) values ($1, $2)")
|
||||
|
||||
(sm/defmutation ::request-profile-recovery
|
||||
[{:keys [email] :as params}]
|
||||
(letfn [(create-recovery-token [conn {:keys [id] :as profile}]
|
||||
(let [token (-> (sodi.prng/random-bytes 32)
|
||||
(sodi.util/bytes->b64s))
|
||||
sql sql:insert-recovery-token]
|
||||
(db/insert! conn :password-recovery-token
|
||||
{:profile-id id
|
||||
:token token})
|
||||
(let [token (-> (sodi.prng/random-bytes 32)
|
||||
(sodi.util/bytes->b64s))
|
||||
payload {:type :password-recovery-token
|
||||
: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)))
|
||||
|
||||
(send-email-notification [conn profile]
|
||||
(emails/send! conn emails/password-recovery
|
||||
{:to (:email profile)
|
||||
:public-url (:public-url cfg/config)
|
||||
:token (:token profile)
|
||||
:name (:fullname profile)})
|
||||
nil)]
|
||||
:name (:fullname profile)}))]
|
||||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [profile (->> (retrieve-profile-by-email conn email)
|
||||
(create-recovery-token conn))]
|
||||
(send-email-notification conn profile)))))
|
||||
(send-email-notification conn profile)
|
||||
nil))))
|
||||
|
||||
|
||||
;; --- Mutation: Recover Profile
|
||||
|
@ -326,27 +451,30 @@
|
|||
(s/def ::recover-profile
|
||||
(s/keys :req-un [::token ::password]))
|
||||
|
||||
(def sql:remove-recovery-token
|
||||
"delete from password_recovery_token where profile_id=$1 and token=$2")
|
||||
|
||||
(sm/defmutation ::recover-profile
|
||||
[{:keys [token password]}]
|
||||
(letfn [(validate-token [conn token]
|
||||
(let [sql "delete from password_recovery_token
|
||||
where token=$1 returning *"
|
||||
sql "select * from password_recovery_token
|
||||
where token=$1"]
|
||||
(-> {:token token}
|
||||
(db/get-by-params conn :password-recovery-token)
|
||||
(:profile-id))))
|
||||
(let [{:keys [token content]}
|
||||
(-> (db/get-by-params conn :generic-token {:token token})
|
||||
(decode-token-row))]
|
||||
(when (not= (:type content) :password-recovery-token)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
(:profile-id content)))
|
||||
|
||||
(update-password [conn profile-id]
|
||||
(let [sql "update profile set password=$2 where id=$1"
|
||||
pwd (sodi.pwhash/derive password)]
|
||||
(db/update! conn :profile {:password pwd} {:id profile-id})
|
||||
nil))]
|
||||
(let [pwd (sodi.pwhash/derive password)]
|
||||
(db/update! conn :profile {:password pwd} {:id profile-id})))
|
||||
|
||||
(delete-token [conn token]
|
||||
(db/delete! conn :generic-token {:token token}))]
|
||||
|
||||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(-> (validate-token conn token)
|
||||
(update-password conn)))))
|
||||
(->> (validate-token conn token)
|
||||
(update-password conn))
|
||||
(delete-token conn token)
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Delete Profile
|
||||
|
@ -391,6 +519,6 @@
|
|||
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
|
||||
(when-not (empty? rows)
|
||||
(ex/raise :type :validation
|
||||
:code :owner-teams-with-people
|
||||
:code ::owner-teams-with-people
|
||||
:hint "The user need to transfer ownership of owned teams."
|
||||
:context {:teams (mapv :team-id rows)}))))
|
||||
|
|
|
@ -73,8 +73,7 @@
|
|||
|
||||
(defn retrieve-profile-data
|
||||
[conn id]
|
||||
(let [sql "select * from profile where id=? and deleted_at is null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
(db/get-by-id conn :profile id))
|
||||
|
||||
(defn retrieve-profile
|
||||
[conn id]
|
||||
|
@ -93,4 +92,4 @@
|
|||
(defn strip-private-attrs
|
||||
"Only selects a publicy visible profile attrs."
|
||||
[o]
|
||||
(select-keys o [:id :fullname :lang :email :created-at :photo :theme :photo-uri]))
|
||||
(dissoc o :password :deleted-at))
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
[clojure.tools.logging :as log]
|
||||
[clojure.walk :as walk]
|
||||
[clojure.java.io :as io]
|
||||
[cuerdas.core :as str]
|
||||
[uxbox.common.exceptions :as ex])
|
||||
(:import
|
||||
java.io.StringReader
|
||||
|
@ -26,7 +27,7 @@
|
|||
(walk/postwalk (fn [x]
|
||||
(cond
|
||||
(instance? clojure.lang.Named x)
|
||||
(name x)
|
||||
(str/camel (name x))
|
||||
|
||||
(instance? clojure.lang.MapEntry x)
|
||||
x
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
java.time.OffsetDateTime
|
||||
java.time.Duration
|
||||
java.util.Date
|
||||
java.time.temporal.TemporalAmount
|
||||
org.apache.logging.log4j.core.util.CronExpression))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -32,6 +33,10 @@
|
|||
[]
|
||||
(Instant/now))
|
||||
|
||||
(defn plus
|
||||
[d ta]
|
||||
(.plus d ^TemporalAmount ta))
|
||||
|
||||
(defn- obj->duration
|
||||
[{:keys [days minutes seconds hours nanos millis]}]
|
||||
(cond-> (Duration/ofMillis (if (int? millis) ^long millis 0))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue