0
Fork 0
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:
Andrey Antukh 2020-10-05 18:17:55 +02:00 committed by Hirunatan
parent f427c6f8b6
commit 03981628b8
11 changed files with 562 additions and 160 deletions

View file

@ -20,7 +20,7 @@
#{:create-demo-profile
:logout
:profile
:verify-profile-token
:verify-token
:recover-profile
:register-profile
:request-profile-recovery

View file

@ -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?)

View file

@ -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))

View file

@ -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

View file

@ -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)))

View file

@ -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))

View file

@ -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)))

View 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))

View file

@ -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

View file

@ -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
)

View file

@ -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]))