;; 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) KALEIDOS INC (ns app.rpc.commands.verify-token (:require [app.common.exceptions :as ex] [app.common.spec :as us] [app.db :as db] [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.quotes :as quotes] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] [app.util.services :as sv] [clojure.spec.alpha :as s])) (s/def ::iss keyword?) (s/def ::exp ::us/inst) (defmulti process-token (fn [_ _ claims] (:iss claims))) (s/def ::verify-token (s/keys :req-un [::token] :opt [::rpc/profile-id])) (sv/defmethod ::verify-token {::rpc/auth false ::doc/added "1.15" ::doc/module :auth} [{:keys [::db/pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] (let [claims (tokens/verify (::main/props cfg) {:token token}) cfg (assoc cfg :conn conn)] (process-token cfg params claims)))) (defmethod process-token :change-email [{:keys [conn] :as cfg} _params {:keys [profile-id email] :as claims}] (let [email (profile/clean-email email)] (when (profile/get-profile-by-email conn email) (ex/raise :type :validation :code :email-already-exists)) (db/update! conn :profile {:email email} {:id profile-id}) (rph/with-meta claims {::audit/name "update-profile-email" ::audit/props {:email email} ::audit/profile-id profile-id}))) (defmethod process-token :verify-email [{:keys [conn] :as cfg} _ {:keys [profile-id] :as claims}] (let [profile (profile/get-profile conn profile-id) claims (assoc claims :profile profile)] (when-not (:is-active profile) (when (not= (:email profile) (:email claims)) (ex/raise :type :validation :code :invalid-token)) (db/update! conn :profile {:is-active true} {:id (:id profile)})) (-> claims (rph/with-transform (session/create-fn cfg profile-id)) (rph/with-meta {::audit/name "verify-profile-email" ::audit/props (audit/profile->props profile) ::audit/profile-id (:id profile)})))) (defmethod process-token :auth [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] (let [profile (profile/get-profile conn profile-id)] (assoc claims :profile profile))) ;; --- Team Invitation (defn- accept-invitation [{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member] (let [;; Update the role if there is an invitation role (or (some-> invitation :role keyword) role) params (merge {:team-id team-id :profile-id (:id member)} (teams/role->params role))] ;; Do not allow blocked users accept invitations. (when (:is-blocked member) (ex/raise :type :restriction :code :profile-blocked)) (quotes/check-quote! conn {::quotes/id ::quotes/profiles-per-team ::quotes/profile-id (:id member) ::quotes/team-id team-id}) ;; Insert the invited member to the team (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) ;; If profile is not yet verified, mark it as verified because ;; accepting an invitation link serves as verification. (when-not (:is-active member) (db/update! conn :profile {:is-active true} {:id (:id member)})) ;; Delete the invitation (db/delete! conn :team-invitation {:team-id team-id :email-to member-email}) (assoc member :is-active true))) (s/def ::spec.team-invitation/profile-id ::us/uuid) (s/def ::spec.team-invitation/role ::us/keyword) (s/def ::spec.team-invitation/team-id ::us/uuid) (s/def ::spec.team-invitation/member-email ::us/email) (s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid)) (s/def ::team-invitation-claims (s/keys :req-un [::iss ::exp ::spec.team-invitation/profile-id ::spec.team-invitation/role ::spec.team-invitation/team-id ::spec.team-invitation/member-email] :opt-un [::spec.team-invitation/member-id])) (defmethod process-token :team-invitation [{:keys [conn] :as cfg} {:keys [::rpc/profile-id token]} {:keys [member-id team-id member-email] :as claims}] (us/verify! ::team-invitation-claims claims) (let [invitation (db/get* conn :team-invitation {:team-id team-id :email-to member-email}) profile (db/get* conn :profile {:id profile-id} {:columns [:id :email]})] (when (nil? invitation) (ex/raise :type :validation :code :invalid-token :hint "no invitation associated with the token")) (if (some? profile) (if (or (= member-id profile-id) (= member-email (:email profile))) ;; if we have logged-in user and it matches the invitation we proceed ;; with accepting the invitation and joining the current profile to the ;; invited team. (let [profile (accept-invitation cfg claims invitation profile)] (-> (assoc claims :state :created) (rph/with-meta {::audit/name "accept-team-invitation" ::audit/profile-id (:id profile) ::audit/props {:team-id (:team-id claims) :role (:role claims) :invitation-id (:id invitation)}}))) (ex/raise :type :validation :code :invalid-token :hint "logged-in user does not matches the invitation")) ;; If we have not logged-in user, and invitation comes with member-id we ;; redirect user to login, if no memeber-id is present in the invitation ;; token, we redirect user the the register page. {:invitation-token token :iss :team-invitation :redirect-to (if member-id :auth-login :auth-register) :state :pending}))) ;; --- Default (defmethod process-token :default [_ _ _] (ex/raise :type :validation :code :invalid-token))