mirror of
https://github.com/penpot/penpot.git
synced 2026-03-23 11:50:49 +01:00
✨ Add nitrate endpoint to delete teams keeping your-penpot projects
This commit is contained in:
@@ -339,6 +339,21 @@
|
||||
;; --- COMMAND: Move project
|
||||
|
||||
(defn move-project
|
||||
"Moves a project from one team to another.
|
||||
|
||||
Performs comprehensive validation including:
|
||||
- Permission checks on both source and destination teams
|
||||
- Team compatibility verification between source and destination
|
||||
- File features compatibility with destination team
|
||||
|
||||
The operation also:
|
||||
- Updates the project's team assignment
|
||||
- Cleans up any broken library relations after the move
|
||||
|
||||
Throws:
|
||||
- :cant-move-to-same-team if trying to move project to its current team
|
||||
- Permission exceptions if user lacks required permissions
|
||||
- Team compatibility exceptions if teams are incompatible"
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id project-id] :as params}]
|
||||
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
|
||||
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
"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]]
|
||||
@@ -18,12 +20,14 @@
|
||||
[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]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]
|
||||
[clojure.set :as set]))
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; ---- API: authenticate
|
||||
|
||||
@@ -212,3 +216,124 @@
|
||||
{:teams (mapv #(select-keys % [:id :name]) teams)
|
||||
:num-files files-count})))))
|
||||
|
||||
|
||||
;; ---- 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:delete-teams
|
||||
"UPDATE team SET deleted_at = ? WHERE id = ANY(?)")
|
||||
|
||||
(def ^:private schema:delete-teams-keeping-your-penpot-projects
|
||||
[:map
|
||||
[:org-name ::sm/text]
|
||||
[:teams [:vector [:map
|
||||
[:id ::sm/uuid]
|
||||
[:is-your-penpot ::sm/boolean]]]]])
|
||||
|
||||
(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."
|
||||
{::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]}]
|
||||
|
||||
(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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user