Add nitrate api for notify org deletion (#8697)

This commit is contained in:
Pablo Alba
2026-03-23 09:59:57 +01:00
committed by GitHub
parent b637f0a917
commit 8406b5e9f8
5 changed files with 70 additions and 126 deletions

View File

@@ -8,8 +8,6 @@
"Internal Nitrate HTTP RPC API. Provides authenticated access to
organization management and token validation endpoints."
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
@@ -20,7 +18,6 @@
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.management :as management]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as doc]
@@ -88,21 +85,28 @@
[:organization-id ::sm/uuid]
[:organization-name ::sm/text]])
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate"
{::doc/added "2.14"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(defn notify-team-change
[cfg team-id team-name organization-id organization-name notification]
(let [msgbus (::mbus/msgbus cfg)]
(mbus/pub! msgbus
;;TODO There is a bug on dashboard with teams notifications.
;;For now we send it to uuid/zero instead of team-id
:topic uuid/zero
:message {:type :team-org-change
:team-id id
:team-id team-id
:team-name team-name
:organization-id organization-id
:organization-name organization-name})))
:organization-name organization-name
:notification notification})))
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate"
{::doc/added "2.14"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(notify-team-change cfg id nil organization-id organization-name nil))
;; ---- API: notify-user-added-to-organization
@@ -219,121 +223,42 @@
;; ---- API: delete-teams-keeping-your-penpot-projects
(def ^:private sql:get-projects-and-default-teams
"Get projects from specified teams along with their team owner's default team information.
This query:
- Selects projects (id, team_id, name) from teams in the provided list
- Gets the profile_id of each team owner
- Gets the default_team_id where projects should be moved
- Only includes teams where the user is owner
- Only includes projects that contain at least one non-deleted file
- Excludes deleted projects and teams"
"SELECT p.id AS project_id,
p.team_id AS source_team_id,
p.name AS project_name,
tpr.profile_id,
pr.default_team_id
FROM project AS p
JOIN team AS tm ON p.team_id = tm.id
JOIN team_profile_rel AS tpr ON tm.id = tpr.team_id
JOIN profile AS pr ON tpr.profile_id = pr.id
WHERE p.team_id = ANY(?)
AND p.deleted_at IS NULL
AND tm.deleted_at IS NULL
AND tpr.is_owner IS TRUE
AND EXISTS (SELECT 1 FROM file f WHERE f.project_id = p.id AND f.deleted_at IS NULL);")
(def ^:private sql:add-prefix-to-teams
"UPDATE team
SET name = ? || name
WHERE id = ANY(?)
RETURNING id, name;")
(def ^:private sql:delete-teams
"UPDATE team SET deleted_at = ? WHERE id = ANY(?)")
(def ^:private schema:delete-teams-keeping-your-penpot-projects
(def ^:private schema:notify-org-deletion
[:map
[:org-name ::sm/text]
[:teams [:vector [:map
[:id ::sm/uuid]
[:is-your-penpot ::sm/boolean]]]]])
[:teams [:vector ::sm/uuid]]])
(def ^:private schema:delete-teams-error
[:map
[:error ::sm/keyword]
[:message ::sm/text]
[:cause ::sm/text]
[:project-id {:optional true} ::sm/uuid]
[:project-name {:optional true} ::sm/text]
[:team-id {:optional true} ::sm/uuid]
[:phase {:optional true} [:enum :move-projects :delete-teams]]])
(def ^:private schema:delete-teams-result
[:or [:= nil] schema:delete-teams-error])
(defn- ^:private clean-org-name
"Clean and sanitize organization name to remove emojis, special characters,
and prevent potential injections. Only allows alphanumeric characters,
spaces, hyphens, underscores, and parentheses."
[org-name]
(when org-name
(-> org-name
str
str/trim
(str/replace #"[^\w\s\-_()]+" "")
(str/replace #"\s+" " ")
str/trim)))
(sv/defmethod ::delete-teams-keeping-your-penpot-projects
"For a list of teams, move the projects of your-penpot teams to the
default team of each team owner, then delete all provided teams."
(sv/defmethod ::notify-org-deletion
"For a list of teams, rename them with the name of the deleted org, and notify
of the deletion to the connected users"
{::doc/added "2.15"
::sm/params schema:delete-teams-keeping-your-penpot-projects
::sm/result schema:delete-teams-result}
[cfg {:keys [teams org-name ::rpc/request-at]}]
::sm/params schema:notify-org-deletion}
[cfg {:keys [teams org-name]}]
(when (seq teams)
(let [cleaned-org-name (if org-name
(-> org-name
str
str/trim
(str/replace #"[^\w\s\-_()]+" "")
(str/replace #"\s+" " ")
str/trim)
"")
org-prefix (str "[" cleaned-org-name "] ")]
(db/tx-run!
cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [ids-array (db/create-array conn "uuid" teams)
;; ---- Rename projects ----
updated-teams (db/exec! conn [sql:add-prefix-to-teams org-prefix ids-array])]
(let [your-penpot-team-ids (into [] (comp (filter :is-your-penpot) d/xf:map-id) teams)
all-team-ids (into [] d/xf:map-id teams)
cleaned-org-name (clean-org-name org-name)
org-prefix (if (str/empty? cleaned-org-name)
"imported: "
(str cleaned-org-name " imported: "))]
(when (seq all-team-ids)
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
;; ---- Move projects ----
(when (seq your-penpot-team-ids)
(let [ids-array (db/create-array conn "uuid" your-penpot-team-ids)
projects (db/exec! conn [sql:get-projects-and-default-teams ids-array])]
(doseq [{:keys [default-team-id profile-id project-id project-name source-team-id]} projects
:when default-team-id]
(try
(management/move-project cfg {:profile-id profile-id
:team-id default-team-id
:project-id project-id})
(db/update! conn :project
{:is-default false
:name (str org-prefix project-name)}
{:id project-id})
(catch Throwable cause
(ex/raise :type :internal
:code :nitrate-project-move-failed
:context {:project-id project-id
:project-name project-name
:team-id source-team-id}
:cause cause))))))
;; ---- Delete teams ----
(try
(let [team-ids-array (db/create-array conn "uuid" all-team-ids)]
(db/exec-one! conn [sql:delete-teams request-at team-ids-array]))
(catch Throwable cause
(ex/raise :type :internal
:code :nitrate-team-deletion-failed
:context {:team-ids all-team-ids}
:cause cause))))))
nil))
;; ---- Notify users ----
(doseq [team updated-teams]
(notify-team-change cfg (:id team) (:name team) nil org-name "dashboard.org-deleted"))))))))

View File

@@ -685,14 +685,27 @@
(modal/hide)))))
(defn handle-change-team-org
[{:keys [team-id organization-id organization-name]}]
[{:keys [team-id team-name organization-id organization-name notification]}]
(ptk/reify ::handle-change-team-org
ptk/WatchEvent
(watch [_ state _]
(let [current-team-id (:current-team-id state)]
(when (and (contains? cf/flags :nitrate)
notification
(= team-id current-team-id))
(rx/of (ntf/show {:content (tr notification organization-name)
:type :toast
:level :info
:timeout nil})))))
ptk/UpdateEvent
(update [_ state]
(if (contains? cf/flags :nitrate)
(d/update-in-when state [:teams team-id] assoc
:organization-id organization-id
:organization-name organization-name)
(d/update-in-when state [:teams team-id]
(fn [team]
(cond-> (assoc team
:organization-id organization-id
:organization-name organization-name)
team-name (assoc :name team-name))))
state))))

View File

@@ -601,7 +601,7 @@
current-org (team->org team)
default-org? (= (:default-team-id profile) (:id current-org))
default-org? (nil? (:organization-id current-org))
show-orgs-menu*
(mf/use-state false)

View File

@@ -338,6 +338,9 @@ msgstr "You're going to restore %s."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restore file"
msgid "dashboard.org-deleted"
msgstr "The %s organization has been deleted."
#: src/app/main/ui/dashboard/placeholder.cljs:41
msgid "dashboard.add-file"
msgstr "Add file"

View File

@@ -347,6 +347,9 @@ msgstr "Vas a restaurar %s."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restaurar archivo"
msgid "dashboard.org-deleted"
msgstr "La organización %s se ha borrado."
#: src/app/main/ui/dashboard/placeholder.cljs:41
msgid "dashboard.add-file"
msgstr "Añadir archivo"