From a04a6dd34cb5b97b411a93aacedd82d671b11779 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 8 May 2026 20:32:59 +0200 Subject: [PATCH] :construction: WIP --- backend/src/app/email.clj | 19 ++++++ backend/src/app/loggers/audit.clj | 94 ++++++++++++++++---------- backend/src/app/rpc/commands/audit.clj | 10 ++- frontend/src/app/main/data/auth.cljs | 26 ++++--- 4 files changed, 100 insertions(+), 49 deletions(-) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index b42206dc93..eacd7c83c1 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -31,6 +31,25 @@ jakarta.mail.Transport java.util.Properties)) +(defn clean + "Clean and normalizes email address string" + [email] + (let [email (str/lower email) + email (if (str/starts-with? email "mailto:") + (subs email 7) + email) + email (if (or (str/starts-with? email "<") + (str/ends-with? email ">")) + (str/trim email "<>") + email)] + email)) + +(defn get-domain + [email] + (let [email (clean email) + [_ domain] (str/split email "@" 2)] + domain)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; EMAIL IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 93558a9987..04b883b4d1 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -16,6 +16,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.email :as email] [app.http :as-alias http] [app.http.access-token :as-alias actoken] [app.loggers.audit.tasks :as-alias tasks] @@ -33,6 +34,23 @@ ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def profile-props + [:id + :is-active + :is-muted + :auth-backend + :email + :default-team-id + :default-project-id + :fullname + :lang]) + +(def reserved-props + #{:session-id + :password + :old-password + :token}) + (defn extract-utm-params "Extracts additional data from params and namespace them under `penpot` ns." @@ -47,17 +65,6 @@ (assoc (->> sk str/kebab (keyword "penpot")) v))))] (reduce-kv process-param {} params))) -(def profile-props - [:id - :is-active - :is-muted - :auth-backend - :email - :default-team-id - :default-project-id - :fullname - :lang]) - (defn profile->props [profile] (-> profile @@ -65,12 +72,6 @@ (merge (:props profile)) (d/without-nils))) -(def reserved-props - #{:session-id - :password - :old-password - :token}) - (defn clean-props [props] (into {} @@ -238,15 +239,17 @@ (some? tnow) (assoc :tracked-at tnow)))) -(def ^:private xf:filter-safe-props +(def ^:private xf:filter-telemetry-props "Transducer that keeps only map entries whose values are UUIDs." - (filter (fn [[_ v]] (uuid? v)))) + (filter (fn [[k v]] + (and (simple-keyword? k) + (or (uuid? v) (boolean? v) (number? v)))))) -(defn filter-safe-props +(defn filter-telemetry-props "Return only UUID-valued keys from a props map. This preserves object relations while maintaining anonymity." [props] - (into {} xf:filter-safe-props props)) + (into {} xf:filter-telemetry-props props)) (defn- append-audit-entry [cfg params] @@ -256,6 +259,32 @@ (update :ip-addr db/inet))] (db/insert! cfg :audit-log params))) +(defn- get-telemetry-context + "Get a telemetry ready safe context" + [{:keys [context]}] + (select-keys context [:initiator + :version + :client-version + :client-user-agent])) + +(defn- get-telemetry-props + "A telemetry specific safe props" + [{:keys [name props] :as params}] + (cond + (or (= name "login-with-oidc") + (= name "login-with-password") + (= name "register-profile") + (= name "update-profile")) + (-> (select-keys props profile-props) + (filter-telemetry-props) + (assoc :lang (:lang props)) + (assoc :auth-backend (:auth-backend props)) + (assoc :email-domain (email/get-domain (:email props))) + (d/without-nils)) + + :else + (filter-telemetry-props props))) + (defn- handle-event! [cfg event] (let [tnow (ct/now) @@ -291,20 +320,15 @@ ;; kept: initiator, version, client-version and client-user-agent. ;; Timestamps are truncated to day precision to avoid leaking exact ;; event timing. - (let [tday (ct/truncate tnow :days) - safe-context (-> (:context params {}) - (select-keys [:initiator - :version - :client-version - :client-user-agent])) - safe-props (filter-safe-props (:props params {})) - params (-> params - (assoc :source "telemetry:backend") - (assoc :props safe-props) - (assoc :context safe-context) - (assoc :ip-addr (db/inet "0.0.0.0")) - (assoc :created-at tday) - (assoc :tracked-at tday))] + (let [event-name (get params :name) + tday (ct/truncate tnow :days) + params (-> params + (assoc :source "telemetry:backend") + (assoc :props (get-telemetry-props params)) + (assoc :context (get-telemetry-context params)) + (assoc :ip-addr (db/inet "0.0.0.0")) + (assoc :created-at tday) + (assoc :tracked-at tday))] (append-audit-entry cfg params)))) (when (and (contains? cf/flags :webhooks) diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index 0aeda09f12..dc2b7a3c1a 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -124,8 +124,12 @@ (comp (map adjust-timestamp) (map (fn [event] - (let [tday (ct/truncate (::audit/created-at event) :days) - safe-props (audit/filter-safe-props (::audit/props event {}))] + (let [tday (-> (::audit/created-at event) + (ct/truncate :days)) + safe-props (-> (::audit/props event {}) + (audit/filter-telemetry-props)) + safe-context (-> (::audit/context event) + (select-keys safe-context-keys))] [(::audit/id event) (::audit/name event) "telemetry:frontend" @@ -135,7 +139,7 @@ (::audit/profile-id event) (db/inet "0.0.0.0") (db/tjson safe-props) - (db/tjson (filter-safe-context (::audit/context event {})))]))))) + (db/tjson safe-context)]))))) (defn- handle-events [{:keys [::db/pool] :as cfg} params] diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index e3aa763ad6..41ff00a6b2 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -61,26 +61,30 @@ (rx/of (dcm/go-to-dashboard-recent {:team-id team-id})))))))] (ptk/reify ::logged-in - ev/Event - (-data [_] - {::ev/name "signin" - ::ev/type "identify" - :email (:email profile) - :auth-backend (:auth-backend profile) - :fullname (:fullname profile) - :is-muted (:is-muted profile) - :default-team-id (:default-team-id profile) - :default-project-id (:default-project-id profile)}) - ptk/WatchEvent (watch [_ _ stream] (cf/initialize-external-context-info) + (->> (rx/merge (rx/of (dp/set-profile profile) (ws/initialize) (dtm/fetch-teams)) + ;; We schedule this event to be executed a bit later, + ;; when the profile is already set + (->> (rx/of (ev/event {::ev/name "signin" + ::ev/type "identify" + :id (:id profile) + :email (:email profile) + :auth-backend (:auth-backend profile) + :fullname (:fullname profile) + :is-muted (:is-muted profile) + :default-team-id (:default-team-id profile) + :default-project-id (:default-project-id profile)})) + (rx/observe-on :async)) + + (->> stream (rx/filter (ptk/type? ::dtm/teams-fetched)) (rx/take 1)