Compare commits

...

26 Commits

Author SHA1 Message Date
Belén Albeza
ffee6c63eb Merge pull request #4841 from penpot/superalex-release-notes-2-1
 Release notes for 2.1
2024-07-04 11:55:25 +02:00
Alejandro Alonso
0ec1bb7a22 Release notes for 2.1 2024-07-04 11:16:20 +02:00
Andrey Antukh
8d6791105a Merge pull request #4833 from penpot/superalex-add-extra-info-to-create-team-response
 Add extra info to create team response
2024-07-02 15:37:42 +02:00
Alejandro Alonso
f051137098 Add extra info to create team response 2024-07-02 13:36:12 +02:00
Andrey Antukh
675a31796c Merge pull request #4823 from penpot/superalex-improve-audit-log-external-origin-events
 improve audit log external origin events
2024-07-02 08:29:33 +02:00
Alejandro Alonso
8dcd538bd2 Improve external origin events for audit_log 2024-07-02 08:14:22 +02:00
Andrey Antukh
384ad2e6fa Merge pull request #4813 from penpot/superalex-tracking-teams-and-invitations-in-posthog
 Add extra events info to track teams and invitations
2024-06-28 11:08:30 +02:00
Andrey Antukh
c090a11e5b Normalize audit event origin tracking on frontend and backend 2024-06-28 10:30:26 +02:00
Alejandro Alonso
f6b367cdca Add extra events info to track teams and invitations 2024-06-28 10:30:26 +02:00
Andrey Antukh
5b9d2663c0 Merge remote-tracking branch 'origin/main' into staging 2024-06-28 08:18:30 +02:00
Alejandro
5e5c105d92 Merge pull request #4817 from penpot/niwinz-hotfix-1
🐛 More fixes on account deletion process
2024-06-28 07:43:07 +02:00
Alejandro
9c2c2fec6a Merge pull request #4815 from penpot/niwinz-bugfix-2
🐛 Backport from main account deletion bugfixes
2024-06-28 07:35:53 +02:00
Andrey Antukh
56160cf64d 🐛 Fix error handling on verify-token page 2024-06-27 16:11:43 +02:00
Andrey Antukh
c45a105186 🐛 Set correct order of execution for logged-out event 2024-06-27 16:11:37 +02:00
Andrey Antukh
f364666d48 🐛 Fix error handling on verify-token page 2024-06-27 16:10:26 +02:00
Andrey Antukh
7facd69039 🐛 Set correct order of execution for logged-out event 2024-06-27 16:10:04 +02:00
Alejandro
b0fea30770 Merge pull request #4814 from penpot/niwinz-hotfix-1
🐛 Add missing logged-out event after account deletion
2024-06-27 14:04:16 +02:00
Andrey Antukh
17015c5353 🐛 Add missing logged-out event after account deletion 2024-06-27 14:00:52 +02:00
Andrey Antukh
ba721def26 Add srepl helpers for profile deletion handling 2024-06-27 14:00:52 +02:00
Andrey Antukh
f9af7f0f09 🐛 Make profile deletion follow the delete-object flow
This removes the need of the specific task for cleaning
orphan teams.
2024-06-27 14:00:52 +02:00
Andrey Antukh
56476acc19 🐛 Fix error handling on account deletion process 2024-06-27 13:56:39 +02:00
Andrey Antukh
67489c0bb9 🐛 Fix profile deletion issue with 1 participant 2024-06-27 13:56:39 +02:00
Andrey Antukh
272edec3c6 🐛 Add missing logged-out event after account deletion 2024-06-27 13:53:30 +02:00
Alejandro
78addf00b4 Merge pull request #4812 from penpot/niwinz-hotfix-1
🐛 Fix account deletion process issues
2024-06-27 13:02:55 +02:00
Andrey Antukh
2cddbc8a3d 🐛 Fix error handling on account deletion process 2024-06-27 12:16:51 +02:00
Andrey Antukh
7e44ae62a2 🐛 Fix profile deletion issue with 1 participant 2024-06-27 12:16:51 +02:00
21 changed files with 493 additions and 184 deletions

View File

@@ -91,6 +91,7 @@
[params]
(d/without-nils
{:external-session-id (::rpc/external-session-id params)
:event-origin (::rpc/external-event-origin params)
:triggered-by (::rpc/handler-name params)}))
;; --- SPECS
@@ -147,18 +148,20 @@
(::rpc/profile-id params)
uuid/zero)
session-id (get params ::rpc/external-session-id)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
session-id (get params ::rpc/external-session-id)
event-origin (get params ::rpc/external-event-origin)
props (-> (or (::replace-props resultm)
(-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props))
(clean-props))
token-id (::actoken/id request)
context (-> (::context resultm)
(assoc :external-session-id session-id)
(assoc :external-event-origin event-origin)
(assoc :access-token-id (some-> token-id str))
(d/without-nils))]

View File

@@ -343,7 +343,6 @@
::wrk/tasks
{:sendmail (ig/ref ::email/handler)
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
:orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler)
:file-gc (ig/ref :app.tasks.file-gc/handler)
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
@@ -388,9 +387,6 @@
{::db/pool (ig/ref ::db/pool)
::sto/storage (ig/ref ::sto/storage)}
:app.tasks.orphan-teams-gc/handler
{::db/pool (ig/ref ::db/pool)}
:app.tasks.delete-object/handler
{::db/pool (ig/ref ::db/pool)}
@@ -479,9 +475,6 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :objects-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :orphan-teams-gc}
{:cron #app/cron "0 0 0 * * ?" ;; daily
:task :storage-gc-deleted}

View File

@@ -80,11 +80,13 @@
(::actoken/profile-id request))
session-id (rreq/get-header request "x-external-session-id")
event-origin (rreq/get-header request "x-event-origin")
data (-> params
(assoc ::handler-name handler-name)
(assoc ::request-at (dt/now))
(assoc ::external-session-id session-id)
(assoc ::external-event-origin event-origin)
(assoc ::session/id (::session/id request))
(assoc ::cond/key etag)
(cond-> (uuid? profile-id)

View File

@@ -28,7 +28,7 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[app.worker :as wrk]
[cuerdas.core :as str]
[promesa.exec :as px]))
@@ -366,13 +366,13 @@
;; --- MUTATION: Delete Profile
(declare ^:private get-owned-teams-with-participants)
(declare ^:private get-owned-teams)
(sv/defmethod ::delete-profile
{::doc/added "1.0"}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(db/with-atomic [conn pool]
(let [teams (get-owned-teams-with-participants conn profile-id)
(let [teams (get-owned-teams conn profile-id)
deleted-at (dt/now)]
;; If we found owned teams with participants, we don't allow
@@ -384,37 +384,39 @@
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :id teams)}))
(doseq [{:keys [id]} teams]
(db/update! conn :team
{:deleted-at deleted-at}
{:id id}))
;; Mark profile deleted immediatelly
(db/update! conn :profile
{:deleted-at deleted-at}
{:id profile-id})
;; Schedule cascade deletion to a worker
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :profile
:deleted-at deleted-at
:id profile-id}})
(rph/with-transform {} (session/delete-fn cfg)))))
;; --- HELPERS
(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 = ?
"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 as id,
count(tpr.profile_id) - 1 as participants
from team_profile_rel as tpr
where tpr.team_id in (select id from owner_teams)
and tpr.profile_id != ?
group by 1")
SELECT tpr.team_id AS id,
count(tpr.profile_id) - 1 AS participants
FROM team_profile_rel AS tpr
WHERE tpr.team_id IN (SELECT id from owner_teams)
GROUP BY 1")
(defn- get-owned-teams-with-participants
(defn get-owned-teams
[conn profile-id]
(db/exec! conn [sql:owned-teams profile-id profile-id]))
(db/exec! conn [sql:owned-teams profile-id]))
(def ^:private sql:profile-existence
"select exists (select * from profile

View File

@@ -357,10 +357,12 @@
::quotes/profile-id profile-id})
(let [features (-> (cfeat/get-enabled-features cf/flags)
(cfeat/check-client-features! (:features params)))]
(create-team cfg (assoc params
:profile-id profile-id
:features features))))))
(cfeat/check-client-features! (:features params)))
team (create-team cfg (assoc params
:profile-id profile-id
:features features))]
(with-meta team
{::audit/props {:id (:id team)}})))))
(defn create-team
"This is a complete team creation process, it creates the team
@@ -867,7 +869,6 @@
:invitations invitations}
{::audit/props {:invitations (count invitations)}})))))
;; --- Mutation: Create Team & Invite Members
(def ^:private schema:create-team-with-invitations
@@ -881,7 +882,7 @@
(sv/defmethod ::create-team-with-invitations
{::doc/added "1.17"
::sm/params schema:create-team-with-invitations}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
(db/with-atomic [conn pool]
(let [features (-> (cfeat/get-enabled-features cf/flags)
@@ -894,7 +895,8 @@
cfg (assoc cfg ::db/conn conn)
team (create-team cfg params)
profile (db/get-by-id conn :profile profile-id)
emails (into #{} (map profile/clean-email) emails)]
emails (into #{} (map profile/clean-email) emails)
context (audit/params->context params)]
;; Create invitations for all provided emails.
(->> emails
@@ -918,6 +920,14 @@
::quotes/team-id (:id team)
::quotes/incr (count emails)}))
(audit/submit! cfg
{::audit/type "action"
::audit/name "create-team"
::audit/profile-id profile-id
::audit/props {:name name
:features features}
::audit/context context})
(audit/submit! cfg
{::audit/type "command"
::audit/name "create-team-invitations"

View File

@@ -147,7 +147,7 @@
(defmethod process-token :team-invitation
[{:keys [conn] :as cfg}
{:keys [::rpc/profile-id token]}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]
(us/verify! ::team-invitation-claims claims)
@@ -169,13 +169,20 @@
;; 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)}})))
(let [context (audit/params->context params)
props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}]
(accept-invitation cfg claims invitation profile)
(audit/submit! cfg
{::audit/type "action"
::audit/name "accept-team-invitation"
::audit/profile-id profile-id
::audit/props props
::audit/context context})
(assoc claims :state :created))
(ex/raise :type :validation
:code :invalid-token

View File

@@ -502,8 +502,6 @@
:restored)
(defn- restore-project*
[{:keys [::db/conn] :as cfg} project-id]
@@ -535,6 +533,24 @@
:restored)
(defn- restore-profile*
[{:keys [::db/conn] :as cfg} profile-id]
(db/update! conn :profile
{:deleted-at nil}
{:id profile-id})
(doseq [{:keys [id]} (profile/get-owned-teams conn profile-id)]
(restore-team* cfg id))
:restored)
(defn restore-deleted-profile!
"Mark a team and all related objects as not deleted"
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(db/tx-run! main/system restore-profile* profile-id)))
(defn restore-deleted-team!
"Mark a team and all related objects as not deleted"
[team-id]
@@ -562,6 +578,15 @@
(assoc ::wrk/params {:object :team
:deleted-at (dt/now)
:id team-id})))))
(defn delete-profile!
"Mark a profile for deletion"
[profile-id]
(let [profile-id (h/parse-uuid profile-id)]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at (dt/now)
:id profile-id})))))
(defn delete-project!
"Mark a project for deletion"
[project-id]
@@ -582,6 +607,15 @@
:deleted-at (dt/now)
:id file-id})))))
(defn process-deleted-profiles-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from profile where deleted_at is not null"])
(run! (fn [{:keys [id deleted-at]}]
(wrk/invoke! (-> main/system
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :profile
:deleted-at deleted-at
:id id})))))))
(defn process-deleted-teams-cascade
[]
@@ -593,7 +627,6 @@
:deleted-at deleted-at
:id id})))))))
(defn process-deleted-projects-cascade
[]
(->> (db/exec! main/system ["select id, deleted_at from project where deleted_at is not null"])

View File

@@ -10,6 +10,8 @@
[app.common.logging :as l]
[app.db :as db]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.util.time :as dt]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
@@ -21,7 +23,9 @@
(defmethod delete-object :file
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})]
(l/trc :hint "marking for deletion" :rel "file" :id (str id))
(l/trc :hint "marking for deletion" :rel "file" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :file
{:deleted-at deleted-at}
{:id id}
@@ -53,7 +57,9 @@
(defmethod delete-object :project
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "project" :id (str id))
(l/trc :hint "marking for deletion" :rel "project" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :project
{:deleted-at deleted-at}
{:id id}
@@ -68,7 +74,8 @@
(defmethod delete-object :team
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "team" :id (str id))
(l/trc :hint "marking for deletion" :rel "team" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :team
{:deleted-at deleted-at}
{:id id}
@@ -87,6 +94,20 @@
:object :project
:deleted-at deleted-at)))))
(defmethod delete-object :profile
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "profile" :id (str id)
:deleted-at (dt/format-instant deleted-at))
(db/update! conn :profile
{:deleted-at deleted-at}
{:id id}
{::db/return-keys false})
(doseq [team (profile/get-owned-teams conn id)]
(delete-object cfg (assoc team
:object :team
:deleted-at deleted-at))))
(defmethod delete-object :default
[_cfg props]

View File

@@ -35,11 +35,6 @@
;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the profile. The
;; relevant objects will be deleted using DELETE
;; CASCADE database triggers. This may leave orphan
;; teams, but there is a special task for deleting
;; orphaned teams.
(db/delete! conn :profile {:id id})
(inc total))
@@ -269,15 +264,15 @@
0)))
(def ^:private deletion-proc-vars
[#'delete-file-media-objects!
[#'delete-profiles!
#'delete-file-media-objects!
#'delete-file-data-fragments!
#'delete-file-object-thumbnails!
#'delete-file-thumbnails!
#'delete-files!
#'delete-projects!
#'delete-fonts!
#'delete-teams!
#'delete-profiles!])
#'delete-teams!])
(defn- execute-proc!
"A generic function that executes the specified proc iterativelly

View File

@@ -1,66 +0,0 @@
;; 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.tasks.orphan-teams-gc
"A maintenance task that performs orphan teams GC."
(:require
[app.common.logging :as l]
[app.db :as db]
[app.util.time :as dt]
[app.worker :as wrk]
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(def ^:private sql:get-orphan-teams
"SELECT t.id
FROM team AS t
LEFT JOIN team_profile_rel AS tpr
ON (t.id = tpr.team_id)
WHERE tpr.profile_id IS NULL
AND t.deleted_at IS NULL
ORDER BY t.created_at ASC
FOR UPDATE OF t
SKIP LOCKED")
(defn- delete-orphan-teams
"Find all orphan teams (with no members) and mark them for
deletion (soft delete)."
[{:keys [::db/conn] :as cfg}]
(let [deleted-at (dt/now)]
(->> (db/cursor conn sql:get-orphan-teams)
(map :id)
(reduce (fn [total team-id]
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
(db/update! conn :team
{:deleted-at deleted-at}
{:id team-id})
(wrk/submit! (-> cfg
(assoc ::wrk/task :delete-object)
(assoc ::wrk/params {:object :team
:deleted-at deleted-at
:id team-id})))
(inc total))
0))))
(defmethod ig/pre-init-spec ::handler [_]
(s/keys :req [::db/pool]))
(defmethod ig/init-key ::handler
[_ cfg]
(fn [{:keys [props] :as task}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(l/inf :hint "gc started" :rollback? (boolean (:rollback? props)))
(let [total (delete-orphan-teams cfg)]
(l/inf :hint "task finished"
:teams total
:rollback? (boolean (:rollback? props)))
(when (:rollback? props)
(db/rollback! conn))
{:processed total})))))

View File

@@ -127,7 +127,7 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))))))
(t/deftest profile-deletion-simple
(t/deftest profile-deletion-1
(let [prof (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
@@ -153,23 +153,22 @@
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out)))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (nil? (:deleted-at row))))
(let [result (th/run-task! :orphan-teams-gc {:min-age 0})]
(t/is (= 1 (:processed result))))
(th/run-pending-tasks!)
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (dt/instant? (:deleted-at row))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof)}
{::db/remove-deleted false})]
(t/is (nil? row)))
;; query profile after delete
(let [params {::th/type :get-profile
::rpc/profile-id (:id prof)}
@@ -178,6 +177,178 @@
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/deftest profile-deletion-2
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
role1 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof2)
:role :editor})]
;; Assert all roles for team
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
(t/is (= 2 (count roles))))
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))))
(t/deftest profile-deletion-3
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
prof3 (th/create-profile* 3)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
role1 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof2)
:role :editor})
role2 (th/create-team-role* {:team-id (:id team1)
:profile-id (:id prof3)
:role :editor})]
;; Assert all roles for team
(let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})]
(t/is (= 3 (count roles))))
;; Request profile to be deleted (it should fail)
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))
;; Leave team by role 1
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof2)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; Request profile to be deleted (it should fail)
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-teams-with-people))))
;; Leave team by role 0 (the default) and reassing owner to role 3
;; without reassinging it (should fail)
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof1)
;; :reassign-to (:id prof3)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :owner-cant-leave-team))))
;; Leave team by role 0 (the default) and reassing owner to role 3
(let [params {::th/type :leave-team
::rpc/profile-id (:id prof1)
:reassign-to (:id prof3)
:id (:id team1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:result out)))
(t/is (nil? (:error out))))
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:error out))))
;; query files after profile soft deletion
(let [params {::th/type :get-project-files
::rpc/profile-id (:id prof1)
:project-id (:default-project-id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= 1 (count (:result out)))))
(th/run-pending-tasks!)
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result))))
(let [row (th/db-get :team
{:id (:default-team-id prof1)}
{::db/remove-deleted false})]
(t/is (nil? row)))
;; query profile after delete
(let [params {::th/type :get-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(let [result (:result out)]
(t/is (= uuid/zero (:id result)))))))
(t/deftest profile-deletion-4
(let [prof1 (th/create-profile* 1)
file1 (th/create-file* 1 {:profile-id (:id prof1)
:project-id (:default-project-id prof1)
:is-shared false})
team1 (th/create-team* 1 {:profile-id (:id prof1)})
team2 (th/create-team* 2 {:profile-id (:id prof1)})]
;; Request profile to be deleted
(let [params {::th/type :delete-profile
::rpc/profile-id (:id prof1)}
out (th/command! params)]
;; (th/print-result! out)
(t/is (= {} (:result out)))
(t/is (nil? (:error out))))
(th/run-pending-tasks!)
(let [rows (th/db-exec! ["select id,name,deleted_at from team where deleted_at is not null"])]
(t/is (= 3 (count rows))))
;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 8 (:processed result))))))
(t/deftest email-blacklist-1
(t/is (false? (email.blacklist/enabled? th/*system*)))
(t/is (true? (email.blacklist/enabled? (assoc th/*system* :app.email/blacklist []))))

View File

@@ -405,12 +405,13 @@
(dm/assert! (string? name))
(ptk/reify ::create-team
ptk/WatchEvent
(watch [_ state _]
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
features (features/get-enabled-features state)]
(->> (rp/cmd! :create-team {:name name :features features})
features (features/get-enabled-features state)
params {:name name :features features}]
(->> (rp/cmd! :create-team (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@@ -421,7 +422,7 @@
[{:keys [name emails role] :as params}]
(ptk/reify ::create-team-with-invitations
ptk/WatchEvent
(watch [_ state _]
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
@@ -430,7 +431,7 @@
:emails emails
:role role
:features features}]
(->> (rp/cmd! :create-team-with-invitations params)
(->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/map team-created)
(rx/catch on-error))))))
@@ -553,12 +554,12 @@
:resend resend?})
ptk/WatchEvent
(watch [_ _ _]
(watch [it _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
params (dissoc params :resend?)]
(->> (rp/cmd! :create-team-invitations params)
(->> (rp/cmd! :create-team-invitations (with-meta params (meta it)))
(rx/tap on-success)
(rx/catch on-error))))))

View File

@@ -569,10 +569,9 @@
on-success identity}} (meta params)]
(->> (rp/cmd! :delete-profile {})
(rx/tap on-success)
(rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1)))
(rx/map logged-out)
(rx/catch on-error))))))
(rx/catch on-error)
(rx/delay-at-least 300))))))
;; --- EVENT: request-profile-recovery

View File

@@ -10,6 +10,7 @@
[app.common.transit :as t]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.events :as-alias ev]
[app.util.http :as http]
[app.util.sse :as sse]
[beicon.v2.core :as rx]
@@ -93,12 +94,12 @@
(= query-params :all) :get
(str/starts-with? nid "get-") :get
:else :post)
request {:method method
:uri (u/join cf/public-uri "api/rpc/command/" nid)
:credentials "include"
:headers {"accept" "application/transit+json,text/event-stream,*/*"
"x-external-session-id" (cf/external-session-id)}
"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:body (when (= method :post)
(if form-data?
(http/form-data params)
@@ -137,7 +138,8 @@
(->> (http/send! {:method :post
:uri uri
:credentials "include"
:headers {"x-external-session-id" (cf/external-session-id)}
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:query params})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
@@ -147,7 +149,8 @@
(->> (http/send! {:method :post
:uri (u/join cf/public-uri "api/export")
:body (http/transit-data (dissoc params :blob?))
:headers {"x-external-session-id" (cf/external-session-id)}
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:credentials "include"
:response-type (if blob? :blob :text)})
(rx/map http/conditional-decode-transit)
@@ -167,7 +170,8 @@
(->> (http/send! {:method :post
:uri (u/join cf/public-uri "api/rpc/command/" (name id))
:credentials "include"
:headers {"x-external-session-id" (cf/external-session-id)}
:headers {"x-external-session-id" (cf/external-session-id)
"x-event-origin" (::ev/origin (meta params))}
:body (http/form-data params)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))

View File

@@ -70,27 +70,28 @@
(rx/subs!
(fn [tdata]
(handle-token tdata))
(fn [{:keys [type code] :as error}]
(cond
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(reset! bad-token true)
(fn [cause]
(let [{:keys [type code] :as error} (ex-data cause)]
(cond
(or (= :validation type)
(= :invalid-token code)
(= :token-expired (:reason error)))
(reset! bad-token true)
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-exists code)
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-validated code)
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (msg/warn msg)))
(st/emit! (rt/nav :auth-login)))
(= :email-already-validated code)
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (msg/warn msg)))
(st/emit! (rt/nav :auth-login)))
:else
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login))))))))
:else
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (msg/error msg)))
(st/emit! (rt/nav :auth-login)))))))))
(if @bad-token
[:> static/invalid-token {}]

View File

@@ -9,6 +9,7 @@
(:require
[app.common.spec :as us]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.messages :as msg]
[app.main.data.modal :as modal]
[app.main.store :as st]
@@ -51,7 +52,8 @@
(let [mdata {:on-success (partial on-create-success form)
:on-error (partial on-error form)}
params {:name (get-in @form [:clean-data :name])}]
(st/emit! (dd/create-team (with-meta params mdata)))))
(st/emit! (-> (dd/create-team (with-meta params mdata))
(with-meta {::ev/origin :dashboard})))))
(defn- on-update-submit
[form]

View File

@@ -100,7 +100,8 @@
(let [mdata {:on-success on-success
:on-error on-error}
params {:name name}]
(st/emit! (dd/create-team (with-meta params mdata))
(st/emit! (-> (dd/create-team (with-meta params mdata))
(with-meta {::ev/origin :onboarding-without-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
:label "team:create-team-and-invite-later"
@@ -115,7 +116,8 @@
(let [mdata {:on-success on-success
:on-error on-error}]
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))
(st/emit! (-> (dd/create-team-with-invitations (with-meta params mdata))
(with-meta {::ev/origin :onboarding-with-invitations}))
(ptk/data-event ::ev/event
{::ev/name "onboarding-step"
:label "team:create-team-and-invite"

View File

@@ -27,6 +27,7 @@
[app.main.ui.releases.v1-8]
[app.main.ui.releases.v1-9]
[app.main.ui.releases.v2-0]
[app.main.ui.releases.v2-1]
[app.util.object :as obj]
[app.util.timers :as tm]
[rumext.v2 :as mf]))
@@ -91,4 +92,4 @@
(defmethod rc/render-release-notes "0.0"
[params]
(rc/render-release-notes (assoc params :version "2.0")))
(rc/render-release-notes (assoc params :version "2.1")))

View File

@@ -0,0 +1,48 @@
;; 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.main.ui.releases.v2-1
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.ui.releases.common :as c]
[rumext.v2 :as mf]))
(defmethod c/render-release-notes "2.1"
[{:keys [slide klass finish version]}]
(mf/html
(case slide
:start
[:div {:class (stl/css-case :modal-overlay true)}
[:div.animated {:class klass}
[:div {:class (stl/css :modal-container)}
[:img {:src "images/features/2.0-intro-image.png"
:class (stl/css :start-image)
:border "0"
:alt "A graphic illustration with Penpot style"}]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-header)}
[:h1 {:class (stl/css :modal-title)}
"What's new in Penpot? "]
[:div {:class (stl/css :version-tag)}
(dm/str "Version " version)]]
[:div {:class (stl/css :features-block)}
[:p {:class (stl/css :feature-content)}
"Penpot 2.1 brings improvements to the authentication system, path editing, real-time persistence, and comments system among other enhancements. Weve improved the stability of the platform by fixing a bunch of bugs, a lot of them raised by our amazing community <3."]
[:p {:class (stl/css :feature-content)}
"This minor release comes shortly after our amazing Penpot 2.0 and it shows the way to long-expected capabilities like the incoming new plugin system!"]
[:p {:class (stl/css :feature-content)}
" Ready to dive in? Let 's get started!"]]
[:div {:class (stl/css :navigation)}
[:button {:class (stl/css :next-btn)
:on-click finish} "Let's go"]]]]]])))

View File

@@ -0,0 +1,79 @@
// 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
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
display: grid;
grid-template-columns: $s-324 1fr;
height: $s-500;
width: $s-888;
border-radius: $br-8;
background-color: var(--modal-background-color);
border: $s-2 solid var(--modal-border-color);
}
.start-image {
width: $s-324;
border-radius: $br-8 0 0 $br-8;
}
.modal-content {
padding: $s-40;
display: grid;
grid-template-rows: auto 1fr $s-32;
gap: $s-24;
}
.modal-header {
display: grid;
gap: $s-8;
}
.version-tag {
@include flexCenter;
@include headlineSmallTypography;
height: $s-32;
width: $s-96;
background-color: var(--communication-tag-background-color);
color: var(--communication-tag-foreground-color);
border-radius: $br-8;
}
.modal-title {
@include headlineLargeTypography;
color: var(--modal-title-foreground-color);
}
.features-block {
display: flex;
flex-direction: column;
gap: $s-16;
width: $s-440;
}
.feature-content {
@include bodyMediumTypography;
margin: 0;
color: var(--modal-text-foreground-color);
}
.navigation {
width: 100%;
display: grid;
grid-template-areas: "bullets button";
}
.next-btn {
@extend .button-primary;
width: $s-100;
justify-self: flex-end;
grid-area: button;
}

View File

@@ -18,11 +18,12 @@
[rumext.v2 :as mf]))
(defn on-error
[{:keys [code] :as error}]
(if (= :owner-teams-with-people code)
(let [msg (tr "notifications.profile-deletion-not-allowed")]
(rx/of (msg/error msg)))
(rx/throw error)))
[cause]
(let [code (-> cause ex-data :code)]
(if (= :owner-teams-with-people code)
(let [msg (tr "notifications.profile-deletion-not-allowed")]
(rx/of (msg/error msg)))
(rx/throw cause))))
(mf/defc delete-account-modal
{::mf/register modal/components