mirror of
https://github.com/penpot/penpot.git
synced 2025-01-09 08:20:45 -05:00
✨ Add additional impl for teams administration.
This commit is contained in:
parent
f427c6f8b6
commit
03981628b8
11 changed files with 562 additions and 160 deletions
|
@ -20,7 +20,7 @@
|
|||
#{:create-demo-profile
|
||||
:logout
|
||||
:profile
|
||||
:verify-profile-token
|
||||
:verify-token
|
||||
:recover-profile
|
||||
:register-profile
|
||||
:request-profile-recovery
|
||||
|
|
|
@ -10,19 +10,19 @@
|
|||
(ns app.media
|
||||
"Media postprocessing."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.media-storage :as mst]
|
||||
[app.util.http :as http]
|
||||
[app.util.storage :as ust]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.core :as fs]
|
||||
[mount.core :refer [defstate]]
|
||||
[app.config :as cfg]
|
||||
[app.common.data :as d]
|
||||
[app.common.media :as cm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.media-storage :as mst]
|
||||
[app.util.storage :as ust]
|
||||
[app.util.http :as http])
|
||||
[mount.core :refer [defstate]])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
java.io.InputStream
|
||||
|
@ -34,6 +34,21 @@
|
|||
(defstate semaphore
|
||||
:start (Semaphore. (:image-process-max-threads cfg/config 1)))
|
||||
|
||||
|
||||
;; --- Generic specs
|
||||
|
||||
(s/def :internal.http.upload/filename ::us/string)
|
||||
(s/def :internal.http.upload/size ::us/integer)
|
||||
(s/def :internal.http.upload/content-type cm/valid-media-types)
|
||||
(s/def :internal.http.upload/tempfile any?)
|
||||
|
||||
(s/def ::upload
|
||||
(s/keys :req-un [:internal.http.upload/filename
|
||||
:internal.http.upload/size
|
||||
:internal.http.upload/tempfile
|
||||
:internal.http.upload/content-type]))
|
||||
|
||||
|
||||
;; --- Thumbnails Generation
|
||||
|
||||
(s/def ::cmd keyword?)
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
(require 'app.services.mutations.projects)
|
||||
(require 'app.services.mutations.files)
|
||||
(require 'app.services.mutations.profile)
|
||||
(require 'app.services.mutations.viewer))
|
||||
(require 'app.services.mutations.viewer)
|
||||
(require 'app.services.mutations.verify-token))
|
||||
|
||||
(defstate query-services
|
||||
:start (load-query-services))
|
||||
|
|
|
@ -46,19 +46,7 @@
|
|||
(declare persist-media-object-on-fs)
|
||||
(declare persist-media-thumbnail-on-fs)
|
||||
|
||||
(s/def :app$upload/filename ::us/string)
|
||||
(s/def :app$upload/size ::us/integer)
|
||||
(s/def :app$upload/content-type cm/valid-media-types)
|
||||
(s/def :app$upload/tempfile any?)
|
||||
|
||||
(s/def ::upload
|
||||
(s/keys :req-un [:app$upload/filename
|
||||
:app$upload/size
|
||||
:app$upload/tempfile
|
||||
:app$upload/content-type]))
|
||||
|
||||
(s/def ::content ::upload)
|
||||
|
||||
(s/def ::content ::media/upload)
|
||||
(s/def ::is-local ::us/boolean)
|
||||
|
||||
(s/def ::add-media-object-from-url
|
||||
|
|
|
@ -20,31 +20,28 @@
|
|||
[app.media-storage :as mst]
|
||||
[app.http.session :as session]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.mutations.media :as media-mutations]
|
||||
[app.services.mutations.projects :as projects]
|
||||
[app.services.mutations.teams :as teams]
|
||||
[app.services.queries.profile :as profile]
|
||||
[app.services.tokens :as tokens]
|
||||
[app.services.mutations.verify-token :refer [process-token]]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.storage :as ust]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[buddy.hashers :as hashers]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]))
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::fullname ::us/string)
|
||||
(s/def ::lang ::us/string)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::lang ::us/not-empty-string)
|
||||
(s/def ::path ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::old-password ::us/string)
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::old-password ::us/not-empty-string)
|
||||
(s/def ::theme ::us/string)
|
||||
|
||||
;; --- Mutation: Register Profile
|
||||
|
@ -52,22 +49,15 @@
|
|||
(declare check-profile-existence!)
|
||||
(declare create-profile)
|
||||
(declare create-profile-relations)
|
||||
(declare email-domain-in-whitelist?)
|
||||
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(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))))
|
||||
(s/keys :req-un [::email ::password ::fullname]
|
||||
:opt-un [::token]))
|
||||
|
||||
(sm/defmutation ::register-profile
|
||||
[params]
|
||||
[{:keys [token] :as params}]
|
||||
(when-not (:registration-enabled cfg/config)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
|
@ -80,25 +70,68 @@
|
|||
(db/with-atomic [conn db/pool]
|
||||
(check-profile-existence! conn params)
|
||||
(let [profile (->> (create-profile conn params)
|
||||
(create-profile-relations conn))
|
||||
token (tokens/generate
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})]
|
||||
(create-profile-relations conn))]
|
||||
|
||||
(emails/send! conn emails/register
|
||||
{:to (:email profile)
|
||||
:name (:fullname profile)
|
||||
:token token})
|
||||
profile)))
|
||||
(if token
|
||||
;; If token comes in params, this is because the user comes
|
||||
;; from team-invitation process; in this case we revalidate
|
||||
;; the token and process the token claims again with the new
|
||||
;; profile data.
|
||||
(let [claims (tokens/verify token {:iss :team-invitation})
|
||||
claims (assoc claims :member-id (:id profile))
|
||||
params (assoc params :profile-id (:id profile))]
|
||||
(process-token conn params claims)
|
||||
|
||||
;; Automatically mark the created profile as active because
|
||||
;; we already have the verification of email with the
|
||||
;; team-invitation token.
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)})
|
||||
|
||||
;; Return profile data and create http session for
|
||||
;; automatically login the profile.
|
||||
(with-meta (assoc profile
|
||||
:is-active true
|
||||
:claims claims)
|
||||
{:transform-response
|
||||
(fn [request response]
|
||||
(let [uagent (get-in request [:headers "user-agent"])
|
||||
id (session/create (:id profile) uagent)]
|
||||
(assoc response
|
||||
:cookies (session/cookies id))))}))
|
||||
|
||||
;; If no token is provided, send a verification email
|
||||
(let [token (tokens/generate
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})]
|
||||
|
||||
(emails/send! conn emails/register
|
||||
{:to (:email profile)
|
||||
:name (:fullname profile)
|
||||
:token token})
|
||||
|
||||
profile)))))
|
||||
|
||||
|
||||
(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))))
|
||||
|
||||
(def ^:private sql:profile-existence
|
||||
"select exists (select * from profile
|
||||
where email = ?
|
||||
and deleted_at is null) as val")
|
||||
|
||||
(defn- check-profile-existence!
|
||||
(defn check-profile-existence!
|
||||
[conn {:keys [email] :as params}]
|
||||
(let [email (str/lower email)
|
||||
result (db/exec-one! conn [sql:profile-existence email])]
|
||||
|
@ -152,8 +185,6 @@
|
|||
|
||||
;; --- Mutation: Login
|
||||
|
||||
(declare retrieve-profile-by-email)
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::scope ::us/string)
|
||||
|
||||
|
@ -182,22 +213,12 @@
|
|||
profile)]
|
||||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [prof (-> (retrieve-profile-by-email conn email)
|
||||
(let [prof (-> (profile/retrieve-profile-data-by-email conn email)
|
||||
(validate-profile)
|
||||
(profile/strip-private-attrs))
|
||||
addt (profile/retrieve-additional-data conn (:id prof))]
|
||||
(merge prof addt)))))
|
||||
|
||||
(def sql:profile-by-email
|
||||
"select * from profile
|
||||
where email=?
|
||||
and deleted_at is null")
|
||||
|
||||
(defn- retrieve-profile-by-email
|
||||
[conn email]
|
||||
(let [email (str/lower email)]
|
||||
(db/exec-one! conn [sql:profile-by-email email])))
|
||||
|
||||
|
||||
;; --- Mutation: Register if not exists
|
||||
|
||||
|
@ -222,7 +243,7 @@
|
|||
(create-profile-relations conn)))]
|
||||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [profile (retrieve-profile-by-email conn email)
|
||||
(let [profile (profile/retrieve-profile-data-by-email conn email)
|
||||
profile (if profile
|
||||
(populate-additional-data conn profile)
|
||||
(register-profile conn params))]
|
||||
|
@ -273,10 +294,9 @@
|
|||
|
||||
;; --- Mutation: Update Photo
|
||||
|
||||
(declare upload-photo)
|
||||
(declare update-profile-photo)
|
||||
|
||||
(s/def ::file ::media-mutations/upload)
|
||||
(s/def ::file ::media/upload)
|
||||
(s/def ::update-profile-photo
|
||||
(s/keys :req-un [::profile-id ::file]))
|
||||
|
||||
|
@ -287,7 +307,7 @@
|
|||
(let [profile (profile/retrieve-profile conn profile-id)
|
||||
_ (media/run {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
photo (upload-photo conn params)]
|
||||
photo (teams/upload-photo conn params)]
|
||||
|
||||
;; Schedule deletion of old photo
|
||||
(when (and (string? (:photo profile))
|
||||
|
@ -297,22 +317,6 @@
|
|||
;; Save new photo
|
||||
(update-profile-photo conn profile-id photo))))
|
||||
|
||||
(defn- upload-photo
|
||||
[conn {:keys [file profile-id]}]
|
||||
(let [prefix (-> (bn/random-bytes 8)
|
||||
(bc/bytes->b64u)
|
||||
(bc/bytes->str))
|
||||
thumb (media/run
|
||||
{:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
:width 256
|
||||
:height 256
|
||||
:input {:path (fs/path (:tempfile file))
|
||||
:mtype (:content-type file)}})
|
||||
name (str prefix (cm/format->extension (:format thumb)))]
|
||||
(ust/save! mst/media-storage name (:data thumb))))
|
||||
|
||||
(defn- update-profile-photo
|
||||
[conn profile-id path]
|
||||
(db/update! conn :profile
|
||||
|
@ -346,63 +350,10 @@
|
|||
:token token})
|
||||
nil)))
|
||||
|
||||
(defn- select-profile-for-update
|
||||
(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.
|
||||
|
||||
(defmulti process-token (fn [conn claims] (:iss claims)))
|
||||
|
||||
(s/def ::verify-profile-token
|
||||
(s/keys :req-un [::token]))
|
||||
|
||||
(sm/defmutation ::verify-profile-token
|
||||
[{:keys [token] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [claims (tokens/verify token)]
|
||||
(process-token conn claims))))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[conn {:keys [profile-id email] :as claims}]
|
||||
(let [profile (select-profile-for-update conn profile-id)]
|
||||
(check-profile-existence! conn {:email email})
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[conn {:keys [profile-id] :as claims}]
|
||||
(let [profile (select-profile-for-update conn profile-id)]
|
||||
(when (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-validated))
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[conn {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
(defmethod process-token :default
|
||||
[conn claims]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
|
||||
;; --- Mutation: Request Profile Recovery
|
||||
|
||||
(s/def ::request-profile-recovery
|
||||
|
@ -425,7 +376,7 @@
|
|||
|
||||
(db/with-atomic [conn db/pool]
|
||||
(some->> email
|
||||
(retrieve-profile-by-email conn)
|
||||
(profile/retrieve-profile-data-by-email conn)
|
||||
(create-recovery-token conn)
|
||||
(send-email-notification conn))
|
||||
nil)))
|
||||
|
|
|
@ -107,18 +107,23 @@
|
|||
|
||||
;; --- Mutation: Toggle Project Pin
|
||||
|
||||
(def ^:private
|
||||
sql:update-project-pin
|
||||
"insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
|
||||
values (?, ?, ?, ?)
|
||||
on conflict (team_id, project_id, profile_id)
|
||||
do update set is_pinned=?")
|
||||
|
||||
(s/def ::is-pinned ::us/boolean)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
|
||||
(s/def ::update-project-pin
|
||||
(s/keys :req-un [::profile-id ::id ::team-id ::is-pinned]))
|
||||
|
||||
(sm/defmutation ::update-project-pin
|
||||
[{:keys [id profile-id team-id is-pinned] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(db/update! conn :team-project-profile-rel
|
||||
{:is-pinned is-pinned}
|
||||
{:profile-id profile-id
|
||||
:project-id id
|
||||
:team-id team-id})
|
||||
(db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
|
||||
nil))
|
||||
|
||||
|
||||
|
|
|
@ -9,14 +9,28 @@
|
|||
|
||||
(ns app.services.mutations.teams
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.emails :as emails]
|
||||
[app.media :as media]
|
||||
[app.media-storage :as mst]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.mutations.projects :as projects]
|
||||
[app.util.blob :as blob]))
|
||||
[app.services.queries.teams :as teams]
|
||||
[app.services.tokens :as tokens]
|
||||
[app.services.queries.profile :as profile]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.storage :as ust]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
|
@ -69,3 +83,236 @@
|
|||
:default? true})]
|
||||
(projects/create-project-profile conn {:project-id (:id proj)
|
||||
:profile-id profile-id})))
|
||||
|
||||
|
||||
;; --- Mutation: Update Team
|
||||
|
||||
(s/def ::update-team
|
||||
(s/keys :req-un [::profile-id ::name ::id]))
|
||||
|
||||
(sm/defmutation ::update-team
|
||||
[{:keys [id name profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(teams/check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :team
|
||||
{:name name}
|
||||
{:id id})
|
||||
nil))
|
||||
|
||||
|
||||
;; --- Mutation: Leave Team
|
||||
|
||||
(s/def ::leave-team
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sm/defmutation ::leave-team
|
||||
[{:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id id)
|
||||
members (teams/retrieve-team-members conn id)]
|
||||
|
||||
(when (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :owner-cant-leave-team
|
||||
:hint "reasing owner before leave"))
|
||||
|
||||
(when-not (> (count members) 1)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-leave-team
|
||||
:context {:members (count members)}))
|
||||
|
||||
(db/delete! conn :team-profile-rel
|
||||
{:profile-id profile-id
|
||||
:team-id id})
|
||||
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Delete Team
|
||||
|
||||
(s/def ::delete-team
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sm/defmutation ::delete-team
|
||||
[{:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [perms (teams/check-edition-permissions! conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
(db/delete! conn :team {:id id})
|
||||
nil)))
|
||||
|
||||
;; --- Mutation: Tean Update Role
|
||||
|
||||
(declare retrieve-team-member)
|
||||
(declare role->params)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::member-id ::us/uuid)
|
||||
(s/def ::role #{:owner :admin :editor :viewer})
|
||||
|
||||
(s/def ::update-team-member-role
|
||||
(s/keys :req-un [::profile-id ::team-id ::member-id ::role]))
|
||||
|
||||
(sm/defmutation ::update-team-member-role
|
||||
[{:keys [team-id profile-id member-id role] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id team-id)
|
||||
|
||||
;; We retrieve all team members instead of query the
|
||||
;; database for a single member. This is just for
|
||||
;; convenience, if this bocomes a bottleneck or problematic,
|
||||
;; we will change it to more efficient fetch mechanims.
|
||||
members (teams/retrieve-team-members conn team-id)
|
||||
member (d/seek #(= member-id (:id %)) members)]
|
||||
|
||||
;; If no member is found, just 404
|
||||
(when-not member
|
||||
(ex/raise :type :not-found
|
||||
:code :member-does-not-exist))
|
||||
|
||||
;; First check if we have permissions to change roles
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
;; Don't allow change role of owner member
|
||||
(when (:is-owner member)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-change-role-to-owner))
|
||||
|
||||
;; Don't allow promote to owner to admin users.
|
||||
(when (and (= role :owner)
|
||||
(not (:is-owner perms)))
|
||||
(ex/raise :type :validation
|
||||
:code :cant-promote-to-owner))
|
||||
|
||||
(let [params (role->params role)]
|
||||
;; Only allow single owner on team
|
||||
(when (and (= role :owner)
|
||||
(:is-owner perms))
|
||||
(db/update! conn :team-profile-rel
|
||||
{:is-owner false}
|
||||
{:team-id team-id
|
||||
:profile-id profile-id}))
|
||||
|
||||
(db/update! conn :team-profile-rel params
|
||||
{:team-id team-id
|
||||
:profile-id member-id})
|
||||
nil))))
|
||||
|
||||
(defn role->params
|
||||
[role]
|
||||
(case role
|
||||
:admin {:is-owner false :is-admin true :can-edit true}
|
||||
:editor {:is-owner false :is-admin false :can-edit true}
|
||||
:owner {:is-owner true :is-admin true :can-edit true}
|
||||
:viewer {:is-owner false :is-admin false :can-edit false}))
|
||||
|
||||
|
||||
;; --- Mutation: Team Update Role
|
||||
|
||||
(s/def ::delete-team-member
|
||||
(s/keys :req-un [::profile-id ::team-id ::member-id]))
|
||||
|
||||
(sm/defmutation ::delete-team-member
|
||||
[{:keys [team-id profile-id member-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(when (= member-id profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :cant-remove-yourself))
|
||||
|
||||
(db/delete! conn :team-profile-rel {:profile-id member-id
|
||||
:team-id team-id})
|
||||
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Update Team Photo
|
||||
|
||||
(declare upload-photo)
|
||||
|
||||
(s/def ::file ::media/upload)
|
||||
(s/def ::update-team-photo
|
||||
(s/keys :req-un [::profile-id ::team-id ::file]))
|
||||
|
||||
(sm/defmutation ::update-team-photo
|
||||
[{:keys [profile-id file team-id] :as params}]
|
||||
(media/validate-media-type (:content-type file))
|
||||
(db/with-atomic [conn db/pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(let [team (teams/retrieve-team conn profile-id team-id)
|
||||
_ (media/run {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
photo (upload-photo conn params)]
|
||||
|
||||
;; Schedule deletion of old photo
|
||||
(when (and (string? (:photo team))
|
||||
(not (str/blank? (:photo team))))
|
||||
(tasks/submit! conn {:name "remove-media"
|
||||
:props {:path (:photo team)}}))
|
||||
;; Save new photo
|
||||
(db/update! conn :team
|
||||
{:photo (str photo)}
|
||||
{:id team-id})
|
||||
|
||||
(assoc team :photo (str photo)))))
|
||||
|
||||
(defn upload-photo
|
||||
[conn {:keys [file profile-id]}]
|
||||
(let [prefix (-> (bn/random-bytes 8)
|
||||
(bc/bytes->b64u)
|
||||
(bc/bytes->str))
|
||||
thumb (media/run
|
||||
{:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
:width 256
|
||||
:height 256
|
||||
:input {:path (fs/path (:tempfile file))
|
||||
:mtype (:content-type file)}})
|
||||
name (str prefix (cm/format->extension (:format thumb)))]
|
||||
(ust/save! mst/media-storage name (:data thumb))))
|
||||
|
||||
|
||||
;; --- Mutation: Invite Member
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::invite-team-member
|
||||
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
|
||||
|
||||
(sm/defmutation ::invite-team-member
|
||||
[{:keys [profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [perms (teams/check-edition-permissions! conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
member (profile/retrieve-profile-data-by-email conn email)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
token (tokens/generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "24h")
|
||||
:profile-id (:id profile)
|
||||
:role role
|
||||
:team-id team-id
|
||||
:member-email (:email member email)
|
||||
:member-id (:id member)})]
|
||||
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
(emails/send! conn emails/invite-to-team
|
||||
{:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token token})
|
||||
nil)))
|
||||
|
|
143
backend/src/app/services/mutations/verify_token.clj
Normal file
143
backend/src/app/services/mutations/verify_token.clj
Normal file
|
@ -0,0 +1,143 @@
|
|||
;; 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 app.services.mutations.verify-token
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.emails :as emails]
|
||||
[app.http.session :as session]
|
||||
[app.media :as media]
|
||||
[app.media-storage :as mst]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.mutations.teams :as teams]
|
||||
[app.services.queries.profile :as profile]
|
||||
[app.services.tokens :as tokens]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.storage :as ust]
|
||||
[app.util.time :as dt]
|
||||
[buddy.hashers :as hashers]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defmulti process-token (fn [conn params claims] (:iss claims)))
|
||||
|
||||
(s/def ::verify-token
|
||||
(s/keys :req-un [::token]
|
||||
:opt-un [::profile-id]))
|
||||
|
||||
(sm/defmutation ::verify-token
|
||||
[{:keys [token] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [claims (tokens/verify token)]
|
||||
(process-token conn params claims))))
|
||||
|
||||
(defmethod process-token :change-email
|
||||
[conn params {:keys [profile-id email] :as claims}]
|
||||
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
|
||||
(when (profile/retrieve-profile-data-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists))
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[conn params {:keys [profile-id] :as claims}]
|
||||
(let [profile (db/get-by-id conn :profile profile-id {:for-update true})]
|
||||
(when (:is-active profile)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-validated))
|
||||
(when (not= (:email profile)
|
||||
(:email claims))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:is-active true}
|
||||
{:id (:id profile)})
|
||||
claims))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[conn params {:keys [profile-id] :as claims}]
|
||||
(let [profile (profile/retrieve-profile conn profile-id)]
|
||||
(assoc claims :profile profile)))
|
||||
|
||||
|
||||
;; --- Team Invitation
|
||||
|
||||
(s/def ::iss keyword?)
|
||||
(s/def ::exp ::us/inst)
|
||||
|
||||
(s/def :internal.tokens.team-invitation/profile-id ::us/uuid)
|
||||
(s/def :internal.tokens.team-invitation/role ::us/keyword)
|
||||
(s/def :internal.tokens.team-invitation/team-id ::us/uuid)
|
||||
(s/def :internal.tokens.team-invitation/member-email ::us/email)
|
||||
(s/def :internal.tokens.team-invitation/member-id (s/nilable ::us/uuid))
|
||||
|
||||
(s/def ::team-invitation-claims
|
||||
(s/keys :req-un [::iss ::exp
|
||||
:internal.tokens.team-invitation/profile-id
|
||||
:internal.tokens.team-invitation/role
|
||||
:internal.tokens.team-invitation/team-id
|
||||
:internal.tokens.team-invitation/member-email]
|
||||
:opt-un [:internal.tokens.team-invitation/member-id]))
|
||||
|
||||
(defmethod process-token :team-invitation
|
||||
[conn {:keys [profile-id token]} {:keys [member-id team-id role] :as claims}]
|
||||
(us/assert ::team-invitation-claims claims)
|
||||
(if (uuid? member-id)
|
||||
(let [params (merge {:team-id team-id
|
||||
:profile-id member-id}
|
||||
(teams/role->params role))
|
||||
claims (assoc claims :state :created)]
|
||||
(db/insert! conn :team-profile-rel params)
|
||||
(if (and (uuid? profile-id)
|
||||
(= member-id profile-id))
|
||||
;; If the current session is already matches the invited
|
||||
;; member, then just return the token and leave the frontend
|
||||
;; app redirect to correct team.
|
||||
claims
|
||||
|
||||
;; If the session does not matches the invited member id,
|
||||
;; replace the session with a new one matching the invited
|
||||
;; member. This techinique should be considered secure because
|
||||
;; the user clicking the link he already has access to the
|
||||
;; email account.
|
||||
(with-meta claims
|
||||
{:transform-response
|
||||
(fn [request response]
|
||||
(let [uagent (get-in request [:headers "user-agent"])
|
||||
id (session/create member-id uagent)]
|
||||
(assoc response
|
||||
:cookies (session/cookies id))))})))
|
||||
|
||||
;; In this case, we waint until frontend app redirect user to
|
||||
;; registeration page, the user is correctly registered and the
|
||||
;; register mutation call us again with the same token to finally
|
||||
;; create the corresponding team-profile relation from the first
|
||||
;; condition of this if.
|
||||
(assoc claims
|
||||
:token token
|
||||
:state :pending)))
|
||||
|
||||
|
||||
;; --- Default
|
||||
|
||||
(defmethod process-token :default
|
||||
[conn params claims]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-token))
|
||||
|
|
@ -2,11 +2,15 @@
|
|||
;; 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 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 app.services.queries.profile
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
|
@ -87,6 +91,18 @@
|
|||
|
||||
profile))
|
||||
|
||||
|
||||
(def sql:profile-by-email
|
||||
"select * from profile
|
||||
where email=?
|
||||
and deleted_at is null")
|
||||
|
||||
(defn retrieve-profile-data-by-email
|
||||
[conn email]
|
||||
(let [email (str/lower email)]
|
||||
(db/exec-one! conn [sql:profile-by-email email])))
|
||||
|
||||
|
||||
;; --- Attrs Helpers
|
||||
|
||||
(defn strip-private-attrs
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
join project as p on (p.id = f.project_id)
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null
|
||||
and f.deleted_at is null
|
||||
window w as (partition by f.project_id order by f.modified_at desc)
|
||||
order by f.modified_at desc
|
||||
)
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
(:is-admin row)
|
||||
(:is-owner row))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
:code :not-authorized))
|
||||
row))
|
||||
|
||||
(defn check-read-permissions!
|
||||
[conn profile-id team-id]
|
||||
|
@ -43,7 +44,8 @@
|
|||
;; when row is found this means that read permission is granted.
|
||||
(when-not row
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
:code :not-authorized))
|
||||
row))
|
||||
|
||||
|
||||
;; --- Query: Teams
|
||||
|
@ -76,9 +78,8 @@
|
|||
(let [defaults (profile/retrieve-additional-data conn profile-id)]
|
||||
(db/exec! conn [sql:teams (:default-team-id defaults) profile-id])))
|
||||
|
||||
;; --- Query: Projec by ID
|
||||
;; --- Query: Team (by ID)
|
||||
|
||||
(declare retrieve-team-projects)
|
||||
(declare retrieve-team)
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
|
@ -90,8 +91,42 @@
|
|||
(with-open [conn (db/open)]
|
||||
(retrieve-team conn profile-id id)))
|
||||
|
||||
(defn- retrieve-team
|
||||
(defn retrieve-team
|
||||
[conn profile-id team-id]
|
||||
(let [defaults (profile/retrieve-additional-data conn profile-id)
|
||||
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")]
|
||||
(db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])))
|
||||
sql (str "WITH teams AS (" sql:teams ") SELECT * FROM teams WHERE id=?")
|
||||
result (db/exec-one! conn [sql (:default-team-id defaults) profile-id team-id])]
|
||||
(when-not result
|
||||
(ex/raise :type :not-found
|
||||
:code :object-does-not-exists))
|
||||
result))
|
||||
|
||||
|
||||
;; --- Query: Team Members
|
||||
|
||||
(declare retrieve-team-members)
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::team-members
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sq/defquery ::team-members
|
||||
[{:keys [profile-id team-id]}]
|
||||
(with-open [conn (db/open)]
|
||||
(check-edition-permissions! conn profile-id team-id)
|
||||
(retrieve-team-members conn team-id)))
|
||||
|
||||
(def sql:team-members
|
||||
"select tp.*,
|
||||
p.id,
|
||||
p.email,
|
||||
p.fullname as name,
|
||||
p.photo,
|
||||
p.is_active
|
||||
from team_profile_rel as tp
|
||||
join profile as p on (p.id = tp.profile_id)
|
||||
where tp.team_id = ?")
|
||||
|
||||
(defn retrieve-team-members
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:team-members team-id]))
|
||||
|
|
Loading…
Reference in a new issue