mirror of
https://github.com/penpot/penpot.git
synced 2026-03-25 12:50:35 +01:00
✨ Add nitrate api for notify org deletion (#8697)
This commit is contained in:
@@ -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"))))))))
|
||||
|
||||
|
||||
@@ -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))))
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user