Unauthenticated /api/pulse/unsubscribe endpoints to allow non-logged-in people to unsubscribe from Alerts/DashboardNotifications.

(ns metabase.api.pulse.unsubscribe
  (:require
   [compojure.core :refer [POST]]
   [metabase.api.common :as api]
   [metabase.config :as config]
   [metabase.email.messages :as messages]
   [metabase.events :as events]
   [metabase.models :refer [PulseChannel]]
   [metabase.models.pulse :as models.pulse]
   [metabase.request.core :as request]
   [metabase.util.i18n :refer [tru]]
   [metabase.util.malli.schema :as ms]
   [throttle.core :as throttle]
   [toucan2.core :as t2]))
(def ^:private throttling-disabled? (config/config-bool :mb-disable-session-throttle))
(def ^:private unsubscribe-throttler (throttle/make-throttler :unsubscribe, :attempts-threshold 50))
(defn- check-hash [pulse-id email hash ip-address]
  (when-not throttling-disabled?
    (throttle/check unsubscribe-throttler ip-address))
  (when (not= hash (messages/generate-pulse-unsubscribe-hash pulse-id email))
    (throw (ex-info (tru "Invalid hash.")
                    {:type        type
                     :status-code 400}))))

/

(api/defendpoint POST 
  "Allow non-users to unsubscribe from pulses/subscriptions, with the hash given through email."
  [:as {{:keys [email hash pulse-id]} :body, :as request}]
  {pulse-id ms/PositiveInt
   email    :string
   hash     :string}
  (check-hash pulse-id email hash (request/ip-address request))
  (t2/with-transaction [_conn]
    (api/let-404 [pulse-channel (t2/select-one PulseChannel :pulse_id pulse-id :channel_type "email")]
      (let [emails (get-in pulse-channel [:details :emails])]
        (if (some #{email} emails)
          (t2/update! PulseChannel (:id pulse-channel) (update-in pulse-channel [:details :emails] #(remove #{email} %)))
          (throw (ex-info (tru "Email for pulse-id doesn't exist.")
                          {:type        type
                           :status-code 400}))))
      (events/publish-event! :event/subscription-unsubscribe {:object {:email email}})
      {:status :success :title (:name (models.pulse/retrieve-notification pulse-id :archived false))})))

/undo

(api/defendpoint POST 
  "Allow non-users to undo an unsubscribe from pulses/subscriptions, with the hash given through email."
  [:as {{:keys [email hash pulse-id]} :body, :as request}]
  {pulse-id ms/PositiveInt
   email    :string
   hash     :string}
  (check-hash pulse-id email hash (request/ip-address request))
  (t2/with-transaction [_conn]
    (api/let-404 [pulse-channel (t2/select-one PulseChannel :pulse_id pulse-id :channel_type "email")]
      (let [emails (get-in pulse-channel [:details :emails])]
        (if (some #{email} emails)
          (throw (ex-info (tru "Email for pulse-id already exists.")
                          {:type        type
                           :status-code 400}))
          (t2/update! PulseChannel (:id pulse-channel) (update-in pulse-channel [:details :emails] conj email))))
      (events/publish-event! :event/subscription-unsubscribe-undo {:object {:email email}})
      {:status :success :title (:name (models.pulse/retrieve-notification pulse-id :archived false))})))
(api/define-routes)