diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 569a62583f..3576a89c99 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -437,7 +437,7 @@ ;; --- Mutation: Delete Profile -(declare check-teams-ownership!) +(declare check-can-delete-profile!) (declare mark-profile-as-deleted!) (s/def ::delete-profile @@ -446,7 +446,7 @@ (sv/defmethod ::delete-profile [{:keys [pool session] :as cfg} {:keys [profile-id] :as params}] (db/with-atomic [conn pool] - (check-teams-ownership! conn profile-id) + (check-can-delete-profile! conn profile-id) ;; Schedule a complete deletion of profile (tasks/submit! conn {:name "delete-profile" @@ -464,24 +464,26 @@ (assoc response :cookies (session/cookies session {:value "" :max-age -1})))}))) -(def ^:private sql:teams-ownership-check - "with teams as ( - select tpr.team_id as id - from team_profile_rel as tpr - where tpr.profile_id = ? - and tpr.is_owner is true +(def sql:owned-teams + "with owner_teams as ( + select tpr.team_id as id + from team_profile_rel as tpr + where tpr.is_owner is true + and tpr.profile_id = ? ) select tpr.team_id, count(tpr.profile_id) as num_profiles from team_profile_rel as tpr - where tpr.team_id in (select id from teams) - group by tpr.team_id - having count(tpr.profile_id) > 1") + where tpr.team_id in (select id from owner_teams) + group by 1") -(defn- check-teams-ownership! +(defn- check-can-delete-profile! [conn profile-id] - (let [rows (db/exec! conn [sql:teams-ownership-check profile-id])] - (when-not (empty? rows) + (let [rows (db/exec! conn [sql:owned-teams profile-id])] + ;; If we found owned teams with more than one profile we don't + ;; allow delete profile until the user properly transfer ownership + ;; or explictly removes all participants from the team. + (when (some #(> (:num-profiles %) 1) rows) (ex/raise :type :validation :code :owner-teams-with-people :hint "The user need to transfer ownership of owned teams." diff --git a/backend/src/app/tasks/delete_profile.clj b/backend/src/app/tasks/delete_profile.clj index da8ef6c537..0d0b5f47ea 100644 --- a/backend/src/app/tasks/delete_profile.clj +++ b/backend/src/app/tasks/delete_profile.clj @@ -5,20 +5,24 @@ ;; 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-2021 UXBOX Labs SL (ns app.tasks.delete-profile "Task for permanent deletion of profiles." (:require [app.common.spec :as us] [app.db :as db] + [app.db.sql :as sql] [app.metrics :as mtx] [clojure.spec.alpha :as s] [clojure.tools.logging :as log] [integrant.core :as ig])) +(declare delete-profile-data) (declare handler) +;; --- INIT + (defmethod ig/pre-init-spec ::handler [_] (s/keys :req-un [::db/pool ::mtx/metrics])) @@ -31,74 +35,52 @@ :help "delete profile task timing"} (mtx/instrument handler)))) -(declare delete-profile-data) -(declare delete-teams) -(declare delete-files) -(declare delete-profile) +;; This task is responsible to permanently delete a profile with all +;; the dependent data. As step (1) we delete all owned teams of the +;; profile (that will cause to delete all underlying projects, files, +;; file_media and mark to be deleted storage_object's used by team, +;; profile and files previously deleted. Then, finally as step (2) we +;; proceed to delete the profile row. +;; +;; The storage_objects marked as deleted will be deleted by the +;; corresponding garbage collector task. (s/def ::profile-id ::us/uuid) -(s/def ::props - (s/keys :req-un [::profile-id])) +(s/def ::props (s/keys :req-un [::profile-id])) (defn handler [{:keys [pool]} {:keys [props] :as task}] (us/verify ::props props) (db/with-atomic [conn pool] (let [id (:profile-id props) - profile (db/get-by-id conn :profile id {:for-update true})] + profile (db/exec-one! conn (sql/select :profile {:id id} {:for-update true}))] (if (or (:is-demo profile) - (not (nil? (:deleted-at profile)))) - (delete-profile-data conn (:id profile)) - (log/warn "Profile " (:id profile) - "does not match constraints for deletion"))))) + (:deleted-at profile)) + (delete-profile-data conn id) + (log/warnf "Profile %s does not match constraints for deletion" id))))) -(defn- delete-profile-data - [conn profile-id] - (log/info "Proceding to delete all data related to profile" profile-id) - (delete-teams conn profile-id) - (delete-files conn profile-id) - (delete-profile conn profile-id)) +;; --- IMPL (def ^:private sql:remove-owned-teams - "with teams as ( - select distinct - tpr.team_id as id - from team_profile_rel as tpr - where tpr.profile_id = ? - and tpr.is_owner is true - ), to_delete_teams as ( - select tpr.team_id as id - from team_profile_rel as tpr - where tpr.team_id in (select id from teams) - group by tpr.team_id - having count(tpr.profile_id) = 1 - ) - delete from team - where id in (select id from to_delete_teams) - returning id") + "delete from team + where id in ( + select tpr.team_id + from team_profile_rel as tpr + where tpr.is_owner is true + and tpr.profile_id = ? + )") (defn- delete-teams [conn profile-id] (db/exec-one! conn [sql:remove-owned-teams profile-id])) -(def ^:private sql:remove-owned-files - "with files_to_delete as ( - select distinct - fpr.file_id as id - from file_profile_rel as fpr - inner join file as f on (fpr.file_id = f.id) - where fpr.profile_id = ? - and fpr.is_owner is true - and f.project_id is null - ) - delete from file - where id in (select id from files_to_delete) - returning id") - -(defn- delete-files - [conn profile-id] - (db/exec-one! conn [sql:remove-owned-files profile-id])) - (defn delete-profile [conn profile-id] (db/delete! conn :profile {:id profile-id})) + +(defn- delete-profile-data + [conn profile-id] + (log/infof "Proceding to delete all data related to profile id = %s" profile-id) + (delete-teams conn profile-id) + (delete-profile conn profile-id)) + diff --git a/frontend/deps.edn b/frontend/deps.edn index 961c7459bf..293be7ad50 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -11,7 +11,7 @@ danlentz/clj-uuid {:mvn/version "0.1.9"} frankiesardo/linked {:mvn/version "1.3.0"} - funcool/beicon {:mvn/version "2020.12.20-1"} + funcool/beicon {:mvn/version "2021.01.29-1"} funcool/cuerdas {:mvn/version "2020.03.26-3"} funcool/okulary {:mvn/version "2020.04.14-0"} funcool/potok {:mvn/version "3.2.0"} diff --git a/frontend/src/app/util/i18n.cljs b/frontend/src/app/util/i18n.cljs index c1d9d4c212..989132af6a 100644 --- a/frontend/src/app/util/i18n.cljs +++ b/frontend/src/app/util/i18n.cljs @@ -57,29 +57,38 @@ [x] (C. x)) +(defn empty-string? + [v] + (or (nil? v) (empty? v))) + (defn t ([locale code] - (let [code (name code) - value (gobj/getValueByKeys translations code locale) - value (if (nil? value) code value)] - (if (array? value) - (aget value 0) - value))) + (let [code (name code) + value (gobj/getValueByKeys translations code locale)] + (if (empty-string? value) + (if (= cfg/default-language locale) + code + (t cfg/default-language code)) + (if (array? value) + (aget value 0) + value)))) ([locale code & args] - (let [code (name code) - value (gobj/getValueByKeys translations code locale) - value (if (nil? value) code value) - plural (first (filter c? args)) - value (if (array? value) - (if (= @plural 1) (aget value 0) (aget value 1)) - value)] - (apply str/format value (map #(if (c? %) @% %) args))))) + (let [code (name code) + value (gobj/getValueByKeys translations code locale)] + (if (empty-string? value) + (if (= cfg/default-language locale) + code + (apply t cfg/default-language code args)) + (let [plural (first (filter c? args)) + value (if (array? value) + (if (= @plural 1) (aget value 0) (aget value 1)) + value)] + (apply str/fmt value (map #(if (c? %) @% %) args))))))) (defn tr ([code] (t @locale code)) ([code & args] (apply t @locale code args))) - ;; DEPRECATED (defn use-locale []