mirror of
https://github.com/penpot/penpot.git
synced 2025-06-06 20:31:38 +02:00
Merge pull request #635 from penpot/niwinz/bounce-handling
Bounce & Complaint handling (on AWS only)
This commit is contained in:
commit
9c0dc54cfe
32 changed files with 1357 additions and 323 deletions
|
@ -3,8 +3,16 @@
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|
||||||
|
- Bounce & Complaint handling.
|
||||||
|
|
||||||
|
|
||||||
### Bugs fixed
|
### Bugs fixed
|
||||||
|
|
||||||
|
- Properly handle errors on github, gitlab and ldap auth backends.
|
||||||
|
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1.2.0-alpha
|
## 1.2.0-alpha
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
|
|
||||||
(ns app.config
|
(ns app.config
|
||||||
"A configuration management."
|
"A configuration management."
|
||||||
|
(:refer-clojure :exclude [get])
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.version :as v]
|
[app.common.version :as v]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[clojure.core :as c]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[environ.core :refer [env]]))
|
[environ.core :refer [env]]))
|
||||||
|
@ -52,6 +54,12 @@
|
||||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||||
|
|
||||||
|
:profile-complaint-max-age (dt/duration {:days 7})
|
||||||
|
:profile-complaint-threshold 2
|
||||||
|
|
||||||
|
:profile-bounce-max-age (dt/duration {:days 7})
|
||||||
|
:profile-bounce-threshold 10
|
||||||
|
|
||||||
:allow-demo-users true
|
:allow-demo-users true
|
||||||
:registration-enabled true
|
:registration-enabled true
|
||||||
:registration-domain-whitelist ""
|
:registration-domain-whitelist ""
|
||||||
|
@ -98,6 +106,11 @@
|
||||||
(s/def ::feedback-enabled ::us/boolean)
|
(s/def ::feedback-enabled ::us/boolean)
|
||||||
(s/def ::feedback-destination ::us/string)
|
(s/def ::feedback-destination ::us/string)
|
||||||
|
|
||||||
|
(s/def ::profile-complaint-max-age ::dt/duration)
|
||||||
|
(s/def ::profile-complaint-threshold ::us/integer)
|
||||||
|
(s/def ::profile-bounce-max-age ::dt/duration)
|
||||||
|
(s/def ::profile-bounce-threshold ::us/integer)
|
||||||
|
|
||||||
(s/def ::error-report-webhook ::us/string)
|
(s/def ::error-report-webhook ::us/string)
|
||||||
|
|
||||||
(s/def ::smtp-enabled ::us/boolean)
|
(s/def ::smtp-enabled ::us/boolean)
|
||||||
|
@ -185,6 +198,10 @@
|
||||||
::ldap-bind-dn
|
::ldap-bind-dn
|
||||||
::ldap-bind-password
|
::ldap-bind-password
|
||||||
::public-uri
|
::public-uri
|
||||||
|
::profile-complaint-threshold
|
||||||
|
::profile-bounce-threshold
|
||||||
|
::profile-complaint-max-age
|
||||||
|
::profile-bounce-max-age
|
||||||
::redis-uri
|
::redis-uri
|
||||||
::registration-domain-whitelist
|
::registration-domain-whitelist
|
||||||
::registration-enabled
|
::registration-enabled
|
||||||
|
@ -247,3 +264,10 @@
|
||||||
|
|
||||||
(def deletion-delay
|
(def deletion-delay
|
||||||
(dt/duration {:days 7}))
|
(dt/duration {:days 7}))
|
||||||
|
|
||||||
|
(defn get
|
||||||
|
"A configuration getter. Helps code be more testable."
|
||||||
|
([key]
|
||||||
|
(c/get config key))
|
||||||
|
([key default]
|
||||||
|
(c/get config key default)))
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
[app.util.transit :as t]
|
[app.util.transit :as t]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[next.jdbc :as jdbc]
|
[next.jdbc :as jdbc]
|
||||||
[next.jdbc.date-time :as jdbc-dt])
|
[next.jdbc.date-time :as jdbc-dt])
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
|
|
||||||
(defmethod ig/init-key ::pool
|
(defmethod ig/init-key ::pool
|
||||||
[_ {:keys [migrations] :as cfg}]
|
[_ {:keys [migrations] :as cfg}]
|
||||||
|
(log/debugf "initialize connection pool %s with uri %s" (:name cfg) (:uri cfg))
|
||||||
(let [pool (create-pool cfg)]
|
(let [pool (create-pool cfg)]
|
||||||
(when (seq migrations)
|
(when (seq migrations)
|
||||||
(with-open [conn (open pool)]
|
(with-open [conn (open pool)]
|
||||||
|
|
|
@ -5,13 +5,15 @@
|
||||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
;; defined by the Mozilla Public License, v. 2.0.
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2020 UXBOX Labs SL
|
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.emails
|
(ns app.emails
|
||||||
"Main api for send emails."
|
"Main api for send emails."
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.db.sql :as sql]
|
||||||
[app.tasks :as tasks]
|
[app.tasks :as tasks]
|
||||||
[app.util.emails :as emails]
|
[app.util.emails :as emails]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
@ -41,6 +43,54 @@
|
||||||
:priority 200
|
:priority 200
|
||||||
:props email})))
|
:props email})))
|
||||||
|
|
||||||
|
|
||||||
|
(def sql:profile-complaint-report
|
||||||
|
"select (select count(*)
|
||||||
|
from profile_complaint_report
|
||||||
|
where type = 'complaint'
|
||||||
|
and profile_id = ?
|
||||||
|
and created_at > now() - ?::interval) as complaints,
|
||||||
|
(select count(*)
|
||||||
|
from profile_complaint_report
|
||||||
|
where type = 'bounce'
|
||||||
|
and profile_id = ?
|
||||||
|
and created_at > now() - ?::interval) as bounces;")
|
||||||
|
|
||||||
|
(defn allow-send-emails?
|
||||||
|
[conn profile]
|
||||||
|
(when-not (:is-muted profile false)
|
||||||
|
(let [complaint-threshold (cfg/get :profile-complaint-threshold)
|
||||||
|
complaint-max-age (cfg/get :profile-complaint-max-age)
|
||||||
|
bounce-threshold (cfg/get :profile-bounce-threshold)
|
||||||
|
bounce-max-age (cfg/get :profile-bounce-max-age)
|
||||||
|
|
||||||
|
{:keys [complaints bounces] :as result}
|
||||||
|
(db/exec-one! conn [sql:profile-complaint-report
|
||||||
|
(:id profile)
|
||||||
|
(db/interval complaint-max-age)
|
||||||
|
(:id profile)
|
||||||
|
(db/interval bounce-max-age)])]
|
||||||
|
|
||||||
|
(and (< complaints complaint-threshold)
|
||||||
|
(< bounces bounce-threshold)))))
|
||||||
|
|
||||||
|
(defn has-complaint-reports?
|
||||||
|
([conn email] (has-complaint-reports? conn email nil))
|
||||||
|
([conn email {:keys [threshold] :or {threshold 1}}]
|
||||||
|
(let [reports (db/exec! conn (sql/select :global-complaint-report
|
||||||
|
{:email email :type "complaint"}
|
||||||
|
{:limit 10}))]
|
||||||
|
(>= (count reports) threshold))))
|
||||||
|
|
||||||
|
(defn has-bounce-reports?
|
||||||
|
([conn email] (has-bounce-reports? conn email nil))
|
||||||
|
([conn email {:keys [threshold] :or {threshold 1}}]
|
||||||
|
(let [reports (db/exec! conn (sql/select :global-complaint-report
|
||||||
|
{:email email :type "bounce"}
|
||||||
|
{:limit 10}))]
|
||||||
|
(>= (count reports) threshold))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Emails
|
;; --- Emails
|
||||||
|
|
||||||
(s/def ::subject ::us/string)
|
(s/def ::subject ::us/string)
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.http.auth :as auth]
|
|
||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.middleware :as middleware]
|
[app.http.middleware :as middleware]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
|
@ -127,6 +126,9 @@
|
||||||
["/dbg"
|
["/dbg"
|
||||||
["/error-by-id/:id" {:get (:error-report-handler cfg)}]]
|
["/error-by-id/:id" {:get (:error-report-handler cfg)}]]
|
||||||
|
|
||||||
|
["/webhooks"
|
||||||
|
["/sns" {:post (:sns-webhook cfg)}]]
|
||||||
|
|
||||||
["/api" {:middleware [[middleware/format-response-body]
|
["/api" {:middleware [[middleware/format-response-body]
|
||||||
[middleware/params]
|
[middleware/params]
|
||||||
[middleware/multipart-params]
|
[middleware/multipart-params]
|
||||||
|
@ -147,9 +149,6 @@
|
||||||
["/github" {:post (:auth-handler github-auth)}]
|
["/github" {:post (:auth-handler github-auth)}]
|
||||||
["/github/callback" {:get (:callback-handler github-auth)}]]
|
["/github/callback" {:get (:callback-handler github-auth)}]]
|
||||||
|
|
||||||
["/login" {:post #(auth/login-handler cfg %)}]
|
|
||||||
["/logout" {:post #(auth/logout-handler cfg %)}]
|
|
||||||
|
|
||||||
["/login-ldap" {:post ldap-auth}]
|
["/login-ldap" {:post ldap-auth}]
|
||||||
|
|
||||||
["/rpc" {:middleware [(:middleware session)]}
|
["/rpc" {:middleware [(:middleware session)]}
|
||||||
|
|
|
@ -1,31 +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/.
|
|
||||||
;;
|
|
||||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
;; defined by the Mozilla Public License, v. 2.0.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) 2020 UXBOX Labs SL
|
|
||||||
|
|
||||||
(ns app.http.auth
|
|
||||||
(:require
|
|
||||||
[app.http.session :as session]))
|
|
||||||
|
|
||||||
(defn login-handler
|
|
||||||
[{:keys [session rpc] :as cfg} request]
|
|
||||||
(let [data (:params request)
|
|
||||||
uagent (get-in request [:headers "user-agent"])
|
|
||||||
method (get-in rpc [:methods :mutation :login])
|
|
||||||
profile (method data)
|
|
||||||
id (session/create! session {:profile-id (:id profile)
|
|
||||||
:user-agent uagent})]
|
|
||||||
{:status 200
|
|
||||||
:cookies (session/cookies session {:value id})
|
|
||||||
:body profile}))
|
|
||||||
|
|
||||||
(defn logout-handler
|
|
||||||
[{:keys [session] :as cfg} request]
|
|
||||||
(session/delete! cfg request)
|
|
||||||
{:status 204
|
|
||||||
:cookies (session/cookies session {:value "" :max-age -1})
|
|
||||||
:body ""})
|
|
|
@ -12,7 +12,6 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.http.session :as session]
|
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.data.json :as json]
|
[clojure.data.json :as json]
|
||||||
|
@ -38,7 +37,6 @@
|
||||||
|
|
||||||
(def scope "user:email")
|
(def scope "user:email")
|
||||||
|
|
||||||
|
|
||||||
(defn- build-redirect-url
|
(defn- build-redirect-url
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [public (u/uri (:public-uri cfg))]
|
(let [public (u/uri (:public-uri cfg))]
|
||||||
|
@ -46,6 +44,7 @@
|
||||||
|
|
||||||
(defn- get-access-token
|
(defn- get-access-token
|
||||||
[cfg state code]
|
[cfg state code]
|
||||||
|
(try
|
||||||
(let [params {:client_id (:client-id cfg)
|
(let [params {:client_id (:client-id cfg)
|
||||||
:client_secret (:client-secret cfg)
|
:client_secret (:client-secret cfg)
|
||||||
:code code
|
:code code
|
||||||
|
@ -55,48 +54,37 @@
|
||||||
:headers {"content-type" "application/x-www-form-urlencoded"
|
:headers {"content-type" "application/x-www-form-urlencoded"
|
||||||
"accept" "application/json"}
|
"accept" "application/json"}
|
||||||
:uri (str token-url)
|
:uri (str token-url)
|
||||||
|
:timeout 6000
|
||||||
:body (u/map->query-string params)}
|
:body (u/map->query-string params)}
|
||||||
res (http/send! req)]
|
res (http/send! req)]
|
||||||
|
|
||||||
(when (not= 200 (:status res))
|
(when (= 200 (:status res))
|
||||||
(ex/raise :type :internal
|
(-> (json/read-str (:body res))
|
||||||
:code :invalid-response-from-github
|
(get "access_token"))))
|
||||||
:context {:status (:status res)
|
|
||||||
:body (:body res)}))
|
(catch Exception e
|
||||||
(try
|
(log/error e "unexpected error on get-access-token")
|
||||||
(let [data (json/read-str (:body res))]
|
nil)))
|
||||||
(get data "access_token"))
|
|
||||||
(catch Throwable e
|
|
||||||
(log/error "unexpected error on parsing response body from github access token request" e)
|
|
||||||
nil))))
|
|
||||||
|
|
||||||
(defn- get-user-info
|
(defn- get-user-info
|
||||||
[token]
|
[token]
|
||||||
|
(try
|
||||||
(let [req {:uri (str user-info-url)
|
(let [req {:uri (str user-info-url)
|
||||||
:headers {"authorization" (str "token " token)}
|
:headers {"authorization" (str "token " token)}
|
||||||
|
:timeout 6000
|
||||||
:method :get}
|
:method :get}
|
||||||
res (http/send! req)]
|
res (http/send! req)]
|
||||||
|
(when (= 200 (:status res))
|
||||||
(when (not= 200 (:status res))
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :invalid-response-from-github
|
|
||||||
:context {:status (:status res)
|
|
||||||
:body (:body res)}))
|
|
||||||
|
|
||||||
(try
|
|
||||||
(let [data (json/read-str (:body res))]
|
(let [data (json/read-str (:body res))]
|
||||||
{:email (get data "email")
|
{:email (get data "email")
|
||||||
:fullname (get data "name")})
|
:fullname (get data "name")})))
|
||||||
(catch Throwable e
|
(catch Exception e
|
||||||
(log/error "unexpected error on parsing response body from github access token request" e)
|
(log/error e "unexpected exception on get-user-info")
|
||||||
nil))))
|
nil)))
|
||||||
|
|
||||||
(defn auth
|
(defn auth
|
||||||
[{:keys [tokens] :as cfg} _request]
|
[{:keys [tokens] :as cfg} _request]
|
||||||
(let [state (tokens :generate
|
(let [state (tokens :generate {:iss :github-oauth :exp (dt/in-future "15m")})
|
||||||
{:iss :github-oauth
|
|
||||||
:exp (dt/in-future "15m")})
|
|
||||||
|
|
||||||
params {:client_id (:client-id cfg/config)
|
params {:client_id (:client-id cfg/config)
|
||||||
:redirect_uri (build-redirect-url cfg)
|
:redirect_uri (build-redirect-url cfg)
|
||||||
:state state
|
:state state
|
||||||
|
@ -109,37 +97,38 @@
|
||||||
|
|
||||||
(defn callback
|
(defn callback
|
||||||
[{:keys [tokens rpc session] :as cfg} request]
|
[{:keys [tokens rpc session] :as cfg} request]
|
||||||
|
(try
|
||||||
(let [state (get-in request [:params :state])
|
(let [state (get-in request [:params :state])
|
||||||
_ (tokens :verify {:token state :iss :github-oauth})
|
_ (tokens :verify {:token state :iss :github-oauth})
|
||||||
info (some->> (get-in request [:params :code])
|
info (some->> (get-in request [:params :code])
|
||||||
(get-access-token cfg state)
|
(get-access-token cfg state)
|
||||||
(get-user-info))]
|
(get-user-info))
|
||||||
|
|
||||||
(when-not info
|
_ (when-not info
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :internal
|
||||||
:code :unable-to-authenticate-with-github))
|
:code :unable-to-auth))
|
||||||
|
|
||||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||||
profile (method-fn {:email (:email info)
|
profile (method-fn {:email (:email info)
|
||||||
|
:backend "github"
|
||||||
:fullname (:fullname info)})
|
:fullname (:fullname info)})
|
||||||
uagent (get-in request [:headers "user-agent"])
|
|
||||||
|
|
||||||
token (tokens :generate
|
token (tokens :generate
|
||||||
{:iss :auth
|
{:iss :auth
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:profile-id (:id profile)})
|
:profile-id (:id profile)})
|
||||||
|
|
||||||
uri (-> (u/uri (:public-uri cfg/config))
|
uri (-> (u/uri (:public-uri cfg/config))
|
||||||
(assoc :path "/#/auth/verify-token")
|
(assoc :path "/#/auth/verify-token")
|
||||||
(assoc :query (u/map->query-string {:token token})))
|
(assoc :query (u/map->query-string {:token token})))
|
||||||
|
sxf ((:create session) (:id profile))
|
||||||
sid (session/create! session {:profile-id (:id profile)
|
rsp {:status 302 :headers {"location" (str uri)} :body ""}]
|
||||||
:user-agent uagent})]
|
(sxf request rsp))
|
||||||
|
(catch Exception _e
|
||||||
|
(let [uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/login")
|
||||||
|
(assoc :query (u/map->query-string {:error "unable-to-auth"})))]
|
||||||
{:status 302
|
{:status 302
|
||||||
:headers {"location" (str uri)}
|
:headers {"location" (str uri)}
|
||||||
:cookies (session/cookies session/cookies {:value sid})
|
:body ""}))))
|
||||||
:body ""})))
|
|
||||||
|
|
||||||
;; --- ENTRY POINT
|
;; --- ENTRY POINT
|
||||||
|
|
||||||
|
|
|
@ -12,42 +12,39 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.http.session :as session]
|
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.data.json :as json]
|
[clojure.data.json :as json]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[clojure.tools.logging :as log]
|
[clojure.tools.logging :as log]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[lambdaisland.uri :as uri]))
|
[lambdaisland.uri :as u]))
|
||||||
|
|
||||||
(def scope "read_user")
|
(def scope "read_user")
|
||||||
|
|
||||||
(defn- build-redirect-url
|
(defn- build-redirect-url
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [public (uri/uri (:public-uri cfg))]
|
(let [public (u/uri (:public-uri cfg))]
|
||||||
(str (assoc public :path "/api/oauth/gitlab/callback"))))
|
(str (assoc public :path "/api/oauth/gitlab/callback"))))
|
||||||
|
|
||||||
|
|
||||||
(defn- build-oauth-uri
|
(defn- build-oauth-uri
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [base-uri (uri/uri (:base-uri cfg))]
|
(let [base-uri (u/uri (:base-uri cfg))]
|
||||||
(assoc base-uri :path "/oauth/authorize")))
|
(assoc base-uri :path "/oauth/authorize")))
|
||||||
|
|
||||||
|
|
||||||
(defn- build-token-url
|
(defn- build-token-url
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [base-uri (uri/uri (:base-uri cfg))]
|
(let [base-uri (u/uri (:base-uri cfg))]
|
||||||
(str (assoc base-uri :path "/oauth/token"))))
|
(str (assoc base-uri :path "/oauth/token"))))
|
||||||
|
|
||||||
|
|
||||||
(defn- build-user-info-url
|
(defn- build-user-info-url
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [base-uri (uri/uri (:base-uri cfg))]
|
(let [base-uri (u/uri (:base-uri cfg))]
|
||||||
(str (assoc base-uri :path "/api/v4/user"))))
|
(str (assoc base-uri :path "/api/v4/user"))))
|
||||||
|
|
||||||
(defn- get-access-token
|
(defn- get-access-token
|
||||||
[cfg code]
|
[cfg code]
|
||||||
|
(try
|
||||||
(let [params {:client_id (:client-id cfg)
|
(let [params {:client_id (:client-id cfg)
|
||||||
:client_secret (:client-secret cfg)
|
:client_secret (:client-secret cfg)
|
||||||
:code code
|
:code code
|
||||||
|
@ -56,44 +53,34 @@
|
||||||
req {:method :post
|
req {:method :post
|
||||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||||
:uri (build-token-url cfg)
|
:uri (build-token-url cfg)
|
||||||
:body (uri/map->query-string params)}
|
:body (u/map->query-string params)}
|
||||||
res (http/send! req)]
|
res (http/send! req)]
|
||||||
|
|
||||||
(when (not= 200 (:status res))
|
(when (= 200 (:status res))
|
||||||
(ex/raise :type :internal
|
(-> (json/read-str (:body res))
|
||||||
:code :invalid-response-from-gitlab
|
(get "access_token"))))
|
||||||
:context {:status (:status res)
|
|
||||||
:body (:body res)}))
|
|
||||||
|
|
||||||
(try
|
|
||||||
(let [data (json/read-str (:body res))]
|
|
||||||
(get data "access_token"))
|
|
||||||
(catch Throwable e
|
|
||||||
(log/error "unexpected error on parsing response body from gitlab access token request" e)
|
|
||||||
nil))))
|
|
||||||
|
|
||||||
|
(catch Exception e
|
||||||
|
(log/error e "unexpected error on get-access-token")
|
||||||
|
nil)))
|
||||||
|
|
||||||
(defn- get-user-info
|
(defn- get-user-info
|
||||||
[cfg token]
|
[cfg token]
|
||||||
|
(try
|
||||||
(let [req {:uri (build-user-info-url cfg)
|
(let [req {:uri (build-user-info-url cfg)
|
||||||
:headers {"Authorization" (str "Bearer " token)}
|
:headers {"Authorization" (str "Bearer " token)}
|
||||||
|
:timeout 6000
|
||||||
:method :get}
|
:method :get}
|
||||||
res (http/send! req)]
|
res (http/send! req)]
|
||||||
|
|
||||||
(when (not= 200 (:status res))
|
(when (= 200 (:status res))
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :invalid-response-from-gitlab
|
|
||||||
:context {:status (:status res)
|
|
||||||
:body (:body res)}))
|
|
||||||
|
|
||||||
(try
|
|
||||||
(let [data (json/read-str (:body res))]
|
(let [data (json/read-str (:body res))]
|
||||||
;; (clojure.pprint/pprint data)
|
|
||||||
{:email (get data "email")
|
{:email (get data "email")
|
||||||
:fullname (get data "name")})
|
:fullname (get data "name")})))
|
||||||
(catch Throwable e
|
|
||||||
(log/error "unexpected error on parsing response body from gitlab access token request" e)
|
(catch Exception e
|
||||||
nil))))
|
(log/error e "unexpected exception on get-user-info")
|
||||||
|
nil)))
|
||||||
|
|
||||||
(defn auth
|
(defn auth
|
||||||
[{:keys [tokens] :as cfg} _request]
|
[{:keys [tokens] :as cfg} _request]
|
||||||
|
@ -105,7 +92,7 @@
|
||||||
:response_type "code"
|
:response_type "code"
|
||||||
:state token
|
:state token
|
||||||
:scope scope}
|
:scope scope}
|
||||||
query (uri/map->query-string params)
|
query (u/map->query-string params)
|
||||||
uri (-> (build-oauth-uri cfg)
|
uri (-> (build-oauth-uri cfg)
|
||||||
(assoc :query query))]
|
(assoc :query query))]
|
||||||
{:status 200
|
{:status 200
|
||||||
|
@ -113,36 +100,38 @@
|
||||||
|
|
||||||
(defn callback
|
(defn callback
|
||||||
[{:keys [tokens rpc session] :as cfg} request]
|
[{:keys [tokens rpc session] :as cfg} request]
|
||||||
|
(try
|
||||||
(let [token (get-in request [:params :state])
|
(let [token (get-in request [:params :state])
|
||||||
_ (tokens :verify {:token token :iss :gitlab-oauth})
|
_ (tokens :verify {:token token :iss :gitlab-oauth})
|
||||||
info (some->> (get-in request [:params :code])
|
info (some->> (get-in request [:params :code])
|
||||||
(get-access-token cfg)
|
(get-access-token cfg)
|
||||||
(get-user-info cfg))]
|
(get-user-info cfg))
|
||||||
|
_ (when-not info
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :unable-to-auth))
|
||||||
|
|
||||||
(when-not info
|
method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||||
(ex/raise :type :authentication
|
|
||||||
:code :unable-to-authenticate-with-gitlab))
|
|
||||||
|
|
||||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
|
||||||
profile (method-fn {:email (:email info)
|
profile (method-fn {:email (:email info)
|
||||||
|
:backend "gitlab"
|
||||||
:fullname (:fullname info)})
|
:fullname (:fullname info)})
|
||||||
uagent (get-in request [:headers "user-agent"])
|
|
||||||
|
|
||||||
token (tokens :generate {:iss :auth
|
token (tokens :generate {:iss :auth
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:profile-id (:id profile)})
|
:profile-id (:id profile)})
|
||||||
|
|
||||||
uri (-> (uri/uri (:public-uri cfg))
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
(assoc :path "/#/auth/verify-token")
|
(assoc :path "/#/auth/verify-token")
|
||||||
(assoc :query (uri/map->query-string {:token token})))
|
(assoc :query (u/map->query-string {:token token})))
|
||||||
|
|
||||||
sid (session/create! session {:profile-id (:id profile)
|
sxf ((:create session) (:id profile))
|
||||||
:user-agent uagent})]
|
rsp {:status 302 :headers {"location" (str uri)} :body ""}]
|
||||||
|
(sxf request rsp))
|
||||||
|
(catch Exception _e
|
||||||
|
(let [uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/login")
|
||||||
|
(assoc :query (u/map->query-string {:error "unable-to-auth"})))]
|
||||||
{:status 302
|
{:status 302
|
||||||
:headers {"location" (str uri)}
|
:headers {"location" (str uri)}
|
||||||
:cookies (session/cookies session {:value sid})
|
:body ""}))))
|
||||||
:body ""})))
|
|
||||||
|
|
||||||
|
|
||||||
(s/def ::client-id ::us/not-empty-string)
|
(s/def ::client-id ::us/not-empty-string)
|
||||||
(s/def ::client-secret ::us/not-empty-string)
|
(s/def ::client-secret ::us/not-empty-string)
|
||||||
|
|
|
@ -11,14 +11,13 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.http.session :as session]
|
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.data.json :as json]
|
[clojure.data.json :as json]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[clojure.tools.logging :as log]
|
[clojure.tools.logging :as log]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[lambdaisland.uri :as uri]))
|
[lambdaisland.uri :as u]))
|
||||||
|
|
||||||
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
|
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@
|
||||||
|
|
||||||
(defn- build-redirect-url
|
(defn- build-redirect-url
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [public (uri/uri (:public-uri cfg))]
|
(let [public (u/uri (:public-uri cfg))]
|
||||||
(str (assoc public :path "/api/oauth/google/callback"))))
|
(str (assoc public :path "/api/oauth/google/callback"))))
|
||||||
|
|
||||||
(defn- get-access-token
|
(defn- get-access-token
|
||||||
|
@ -44,7 +43,7 @@
|
||||||
req {:method :post
|
req {:method :post
|
||||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||||
:uri "https://oauth2.googleapis.com/token"
|
:uri "https://oauth2.googleapis.com/token"
|
||||||
:body (uri/map->query-string params)}
|
:body (u/map->query-string params)}
|
||||||
res (http/send! req)]
|
res (http/send! req)]
|
||||||
|
|
||||||
(when (= 200 (:status res))
|
(when (= 200 (:status res))
|
||||||
|
@ -80,8 +79,8 @@
|
||||||
:response_type "code"
|
:response_type "code"
|
||||||
:redirect_uri (build-redirect-url cfg)
|
:redirect_uri (build-redirect-url cfg)
|
||||||
:client_id (:client-id cfg)}
|
:client_id (:client-id cfg)}
|
||||||
query (uri/map->query-string params)
|
query (u/map->query-string params)
|
||||||
uri (-> (uri/uri base-goauth-uri)
|
uri (-> (u/uri base-goauth-uri)
|
||||||
(assoc :query query))]
|
(assoc :query query))]
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:redirect-uri (str uri)}}))
|
:body {:redirect-uri (str uri)}}))
|
||||||
|
@ -99,25 +98,22 @@
|
||||||
:code :unable-to-auth))
|
:code :unable-to-auth))
|
||||||
method-fn (get-in rpc [:methods :mutation :login-or-register])
|
method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||||
profile (method-fn {:email (:email info)
|
profile (method-fn {:email (:email info)
|
||||||
|
:backend "google"
|
||||||
:fullname (:fullname info)})
|
:fullname (:fullname info)})
|
||||||
uagent (get-in request [:headers "user-agent"])
|
|
||||||
token (tokens :generate {:iss :auth
|
token (tokens :generate {:iss :auth
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:profile-id (:id profile)})
|
:profile-id (:id profile)})
|
||||||
uri (-> (uri/uri (:public-uri cfg))
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
(assoc :path "/#/auth/verify-token")
|
(assoc :path "/#/auth/verify-token")
|
||||||
(assoc :query (uri/map->query-string {:token token})))
|
(assoc :query (u/map->query-string {:token token})))
|
||||||
|
|
||||||
sid (session/create! session {:profile-id (:id profile)
|
sxf ((:create session) (:id profile))
|
||||||
:user-agent uagent})]
|
rsp {:status 302 :headers {"location" (str uri)} :body ""}]
|
||||||
{:status 302
|
(sxf request rsp))
|
||||||
:headers {"location" (str uri)}
|
|
||||||
:cookies (session/cookies session {:value sid})
|
|
||||||
:body ""})
|
|
||||||
(catch Exception _e
|
(catch Exception _e
|
||||||
(let [uri (-> (uri/uri (:public-uri cfg))
|
(let [uri (-> (u/uri (:public-uri cfg))
|
||||||
(assoc :path "/#/auth/login")
|
(assoc :path "/#/auth/login")
|
||||||
(assoc :query (uri/map->query-string {:error "unable-to-auth"})))]
|
(assoc :query (u/map->query-string {:error "unable-to-auth"})))]
|
||||||
{:status 302
|
{:status 302
|
||||||
:headers {"location" (str uri)}
|
:headers {"location" (str uri)}
|
||||||
:body ""}))))
|
:body ""}))))
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.http.session :as session]
|
|
||||||
[clj-ldap.client :as client]
|
[clj-ldap.client :as client]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
@ -65,13 +64,12 @@
|
||||||
:password (:password data)))]
|
:password (:password data)))]
|
||||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||||
profile (method-fn {:email (:email info)
|
profile (method-fn {:email (:email info)
|
||||||
|
:backend "ldap"
|
||||||
:fullname (:fullname info)})
|
:fullname (:fullname info)})
|
||||||
uagent (get-in request [:headers "user-agent"])
|
|
||||||
sid (session/create! session {:profile-id (:id profile)
|
sxf ((:create session) (:id profile))
|
||||||
:user-agent uagent})]
|
rsp {:status 200 :body profile}]
|
||||||
{:status 200
|
(sxf request rsp)))))
|
||||||
:cookies (session/cookies session {:value sid})
|
|
||||||
:body profile}))))
|
|
||||||
{::conn conn})))
|
{::conn conn})))
|
||||||
|
|
||||||
(defmethod ig/halt-key! ::client
|
(defmethod ig/halt-key! ::client
|
||||||
|
|
207
backend/src/app/http/awsns.clj
Normal file
207
backend/src/app/http/awsns.clj
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
|
||||||
|
(ns app.http.awsns
|
||||||
|
"AWS SNS webhook handler for bounces."
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.db.sql :as sql]
|
||||||
|
[app.util.http :as http]
|
||||||
|
[clojure.pprint :refer [pprint]]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[integrant.core :as ig]
|
||||||
|
[jsonista.core :as j]))
|
||||||
|
|
||||||
|
(declare parse-json)
|
||||||
|
(declare parse-notification)
|
||||||
|
(declare process-report)
|
||||||
|
|
||||||
|
(defn- pprint-report
|
||||||
|
[message]
|
||||||
|
(binding [clojure.pprint/*print-right-margin* 120]
|
||||||
|
(with-out-str (pprint message))))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req-un [::db/pool]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ cfg]
|
||||||
|
(fn [request]
|
||||||
|
(let [body (parse-json (slurp (:body request)))
|
||||||
|
mtype (get body "Type")]
|
||||||
|
(cond
|
||||||
|
(= mtype "SubscriptionConfirmation")
|
||||||
|
(let [surl (get body "SubscribeURL")
|
||||||
|
stopic (get body "TopicArn")]
|
||||||
|
(log/infof "Subscription received (topic=%s, url=%s)" stopic surl)
|
||||||
|
(http/send! {:uri surl :method :post :timeout 10000}))
|
||||||
|
|
||||||
|
(= mtype "Notification")
|
||||||
|
(when-let [message (parse-json (get body "Message"))]
|
||||||
|
;; (log/infof "Received: %s" (pr-str message))
|
||||||
|
(let [notification (parse-notification cfg message)]
|
||||||
|
(process-report cfg notification)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(log/warn (str "Unexpected data received.\n"
|
||||||
|
(pprint-report body))))
|
||||||
|
|
||||||
|
{:status 200 :body ""})))
|
||||||
|
|
||||||
|
(defn- parse-bounce
|
||||||
|
[data]
|
||||||
|
{:type "bounce"
|
||||||
|
:kind (str/lower (get data "bounceType"))
|
||||||
|
:category (str/lower (get data "bounceSubType"))
|
||||||
|
:feedback-id (get data "feedbackId")
|
||||||
|
:timestamp (get data "timestamp")
|
||||||
|
:recipients (->> (get data "bouncedRecipients")
|
||||||
|
(mapv (fn [item]
|
||||||
|
{:email (str/lower (get item "emailAddress"))
|
||||||
|
:status (get item "status")
|
||||||
|
:action (get item "action")
|
||||||
|
:dcode (get item "diagnosticCode")})))})
|
||||||
|
|
||||||
|
(defn- parse-complaint
|
||||||
|
[data]
|
||||||
|
{:type "complaint"
|
||||||
|
:user-agent (get data "userAgent")
|
||||||
|
:kind (get data "complaintFeedbackType")
|
||||||
|
:category (get data "complaintSubType")
|
||||||
|
:timestamp (get data "arrivalDate")
|
||||||
|
:feedback-id (get data "feedbackId")
|
||||||
|
:recipients (->> (get data "complainedRecipients")
|
||||||
|
(mapv #(get % "emailAddress"))
|
||||||
|
(mapv str/lower))})
|
||||||
|
|
||||||
|
(defn- extract-headers
|
||||||
|
[mail]
|
||||||
|
(reduce (fn [acc item]
|
||||||
|
(let [key (get item "name")
|
||||||
|
val (get item "value")]
|
||||||
|
(assoc acc (str/lower key) val)))
|
||||||
|
{}
|
||||||
|
(get mail "headers")))
|
||||||
|
|
||||||
|
(defn- extract-identity
|
||||||
|
[{:keys [tokens] :as cfg} headers]
|
||||||
|
(let [tdata (get headers "x-penpot-data")]
|
||||||
|
(when-not (str/empty? tdata)
|
||||||
|
(let [result (tokens :verify {:token tdata :iss :profile-identity})]
|
||||||
|
(:profile-id result)))))
|
||||||
|
|
||||||
|
(defn- parse-notification
|
||||||
|
[cfg message]
|
||||||
|
(let [type (get message "notificationType")
|
||||||
|
data (case type
|
||||||
|
"Bounce" (parse-bounce (get message "bounce"))
|
||||||
|
"Complaint" (parse-complaint (get message "complaint"))
|
||||||
|
{:type (keyword (str/lower type))
|
||||||
|
:message message})]
|
||||||
|
(when data
|
||||||
|
(let [mail (get message "mail")]
|
||||||
|
(when-not mail
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :incomplete-notification
|
||||||
|
:hint "no email data received, please enable full headers report"))
|
||||||
|
(let [headers (extract-headers mail)
|
||||||
|
mail {:destination (get mail "destination")
|
||||||
|
:source (get mail "source")
|
||||||
|
:timestamp (get mail "timestamp")
|
||||||
|
:subject (get-in mail ["commonHeaders" "subject"])
|
||||||
|
:headers headers}]
|
||||||
|
(assoc data
|
||||||
|
:mail mail
|
||||||
|
:profile-id (extract-identity cfg headers)))))))
|
||||||
|
|
||||||
|
(defn- parse-json
|
||||||
|
[v]
|
||||||
|
(ex/ignoring
|
||||||
|
(j/read-value v)))
|
||||||
|
|
||||||
|
(defn- register-bounce-for-profile
|
||||||
|
[{:keys [pool]} {:keys [type kind profile-id] :as report}]
|
||||||
|
(when (= kind "permanent")
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(db/insert! conn :profile-complaint-report
|
||||||
|
{:profile-id profile-id
|
||||||
|
:type (name type)
|
||||||
|
:content (db/tjson report)})
|
||||||
|
|
||||||
|
;; TODO: maybe also try to find profiles by mail and if exists
|
||||||
|
;; register profile reports for them?
|
||||||
|
(doseq [recipient (:recipients report)]
|
||||||
|
(db/insert! conn :global-complaint-report
|
||||||
|
{:email (:email recipient)
|
||||||
|
:type (name type)
|
||||||
|
:content (db/tjson report)}))
|
||||||
|
|
||||||
|
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
||||||
|
(when (some #(= (:email profile) (:email %)) (:recipients report))
|
||||||
|
;; If the report matches the profile email, this means that
|
||||||
|
;; the report is for itself, can be caused when a user
|
||||||
|
;; registers with an invalid email or the user email is
|
||||||
|
;; permanently rejecting receiving the email. In this case we
|
||||||
|
;; have no option to mark the user as muted (and in this case
|
||||||
|
;; the profile will be also inactive.
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:is-muted true}
|
||||||
|
{:id profile-id}))))))
|
||||||
|
|
||||||
|
(defn- register-complaint-for-profile
|
||||||
|
[{:keys [pool]} {:keys [type profile-id] :as report}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(db/insert! conn :profile-complaint-report
|
||||||
|
{:profile-id profile-id
|
||||||
|
:type (name type)
|
||||||
|
:content (db/tjson report)})
|
||||||
|
|
||||||
|
;; TODO: maybe also try to find profiles by email and if exists
|
||||||
|
;; register profile reports for them?
|
||||||
|
(doseq [email (:recipients report)]
|
||||||
|
(db/insert! conn :global-complaint-report
|
||||||
|
{:email email
|
||||||
|
:type (name type)
|
||||||
|
:content (db/tjson report)}))
|
||||||
|
|
||||||
|
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
||||||
|
(when (some #(= % (:email profile)) (:recipients report))
|
||||||
|
;; If the report matches the profile email, this means that
|
||||||
|
;; the report is for itself, rare case but can happen; In this
|
||||||
|
;; case just mark profile as muted (very rare case).
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:is-muted true}
|
||||||
|
{:id profile-id})))))
|
||||||
|
|
||||||
|
(defn- process-report
|
||||||
|
[cfg {:keys [type profile-id] :as report}]
|
||||||
|
(log/debug (str "Procesing report:\n" (pprint-report report)))
|
||||||
|
(cond
|
||||||
|
;; In this case we receive a bounce/complaint notification without
|
||||||
|
;; confirmed identity, we just emit a warning but do nothing about
|
||||||
|
;; it because this is not a normal case. All notifications should
|
||||||
|
;; come with profile identity.
|
||||||
|
(nil? profile-id)
|
||||||
|
(log/warn (str "A notification without identity recevied from AWS\n"
|
||||||
|
(pprint-report report)))
|
||||||
|
|
||||||
|
(= "bounce" type)
|
||||||
|
(register-bounce-for-profile cfg report)
|
||||||
|
|
||||||
|
(= "complaint" type)
|
||||||
|
(register-complaint-for-profile cfg report)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(log/warn (str "Unrecognized report received from AWS\n"
|
||||||
|
(pprint-report report)))))
|
||||||
|
|
||||||
|
|
|
@ -16,14 +16,16 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(defn next-session-id
|
;; --- IMPL
|
||||||
|
|
||||||
|
(defn- next-session-id
|
||||||
([] (next-session-id 96))
|
([] (next-session-id 96))
|
||||||
([n]
|
([n]
|
||||||
(-> (bn/random-nonce n)
|
(-> (bn/random-nonce n)
|
||||||
(bc/bytes->b64u)
|
(bc/bytes->b64u)
|
||||||
(bc/bytes->str))))
|
(bc/bytes->str))))
|
||||||
|
|
||||||
(defn create!
|
(defn- create
|
||||||
[{:keys [conn] :as cfg} {:keys [profile-id user-agent]}]
|
[{:keys [conn] :as cfg} {:keys [profile-id user-agent]}]
|
||||||
(let [id (next-session-id)]
|
(let [id (next-session-id)]
|
||||||
(db/insert! conn :http-session {:id id
|
(db/insert! conn :http-session {:id id
|
||||||
|
@ -31,28 +33,28 @@
|
||||||
:user-agent user-agent})
|
:user-agent user-agent})
|
||||||
id))
|
id))
|
||||||
|
|
||||||
(defn delete!
|
(defn- delete
|
||||||
[{:keys [conn cookie-name] :as cfg} request]
|
[{:keys [conn cookie-name] :as cfg} {:keys [cookies] :as request}]
|
||||||
(when-let [token (get-in request [:cookies cookie-name :value])]
|
(when-let [token (get-in cookies [cookie-name :value])]
|
||||||
(db/delete! conn :http-session {:id token}))
|
(db/delete! conn :http-session {:id token}))
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
(defn retrieve
|
(defn- retrieve
|
||||||
[{:keys [conn] :as cfg} token]
|
[{:keys [conn] :as cfg} token]
|
||||||
(when token
|
(when token
|
||||||
(-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token])
|
(-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token])
|
||||||
(:profile-id))))
|
(:profile-id))))
|
||||||
|
|
||||||
(defn retrieve-from-request
|
(defn- retrieve-from-request
|
||||||
[{:keys [cookie-name] :as cfg} request]
|
[{:keys [cookie-name] :as cfg} {:keys [cookies] :as request}]
|
||||||
(->> (get-in request [:cookies cookie-name :value])
|
(->> (get-in cookies [cookie-name :value])
|
||||||
(retrieve cfg)))
|
(retrieve cfg)))
|
||||||
|
|
||||||
(defn cookies
|
(defn- cookies
|
||||||
[{:keys [cookie-name] :as cfg} vals]
|
[{:keys [cookie-name] :as cfg} vals]
|
||||||
{cookie-name (merge vals {:path "/" :http-only true})})
|
{cookie-name (merge vals {:path "/" :http-only true})})
|
||||||
|
|
||||||
(defn middleware
|
(defn- middleware
|
||||||
[cfg handler]
|
[cfg handler]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(if-let [profile-id (retrieve-from-request cfg request)]
|
(if-let [profile-id (retrieve-from-request cfg request)]
|
||||||
|
@ -61,6 +63,8 @@
|
||||||
(handler (assoc request :profile-id profile-id)))
|
(handler (assoc request :profile-id profile-id)))
|
||||||
(handler request))))
|
(handler request))))
|
||||||
|
|
||||||
|
;; --- STATE INIT
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::session [_]
|
(defmethod ig/pre-init-spec ::session [_]
|
||||||
(s/keys :req-un [::db/pool]))
|
(s/keys :req-un [::db/pool]))
|
||||||
|
|
||||||
|
@ -71,4 +75,17 @@
|
||||||
(defmethod ig/init-key ::session
|
(defmethod ig/init-key ::session
|
||||||
[_ {:keys [pool] :as cfg}]
|
[_ {:keys [pool] :as cfg}]
|
||||||
(let [cfg (assoc cfg :conn pool)]
|
(let [cfg (assoc cfg :conn pool)]
|
||||||
(merge cfg {:middleware #(middleware cfg %)})))
|
(-> cfg
|
||||||
|
(assoc :middleware #(middleware cfg %))
|
||||||
|
(assoc :create (fn [profile-id]
|
||||||
|
(fn [request response]
|
||||||
|
(let [uagent (get-in request [:headers "user-agent"])
|
||||||
|
value (create cfg {:profile-id profile-id :user-agent uagent})]
|
||||||
|
(assoc response :cookies (cookies cfg {:value value}))))))
|
||||||
|
(assoc :delete (fn [request response]
|
||||||
|
(delete cfg request)
|
||||||
|
(assoc response
|
||||||
|
:status 204
|
||||||
|
:body ""
|
||||||
|
:cookies (cookies cfg {:value "" :max-age -1})))))))
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,10 @@
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:cookie-name "auth-token"}
|
:cookie-name "auth-token"}
|
||||||
|
|
||||||
|
:app.http.awsns/handler
|
||||||
|
{:tokens (ig/ref :app.tokens/tokens)
|
||||||
|
:pool (ig/ref :app.db/pool)}
|
||||||
|
|
||||||
:app.http/server
|
:app.http/server
|
||||||
{:port (:http-server-port config)
|
{:port (:http-server-port config)
|
||||||
:handler (ig/ref :app.http/router)
|
:handler (ig/ref :app.http/router)
|
||||||
|
@ -90,6 +94,7 @@
|
||||||
:assets (ig/ref :app.http.assets/handlers)
|
:assets (ig/ref :app.http.assets/handlers)
|
||||||
:svgparse (ig/ref :app.svgparse/handler)
|
:svgparse (ig/ref :app.svgparse/handler)
|
||||||
:storage (ig/ref :app.storage/storage)
|
:storage (ig/ref :app.storage/storage)
|
||||||
|
:sns-webhook (ig/ref :app.http.awsns/handler)
|
||||||
:error-report-handler (ig/ref :app.error-reporter/handler)}
|
:error-report-handler (ig/ref :app.error-reporter/handler)}
|
||||||
|
|
||||||
:app.http.assets/handlers
|
:app.http.assets/handlers
|
||||||
|
|
|
@ -148,6 +148,10 @@
|
||||||
|
|
||||||
{:name "0045-add-index-to-file-change-table"
|
{:name "0045-add-index-to-file-change-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
|
:fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
|
||||||
|
|
||||||
|
{:name "0046-add-profile-complaint-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0046-add-profile-complaint-table.sql")}
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
CREATE TABLE profile_complaint_report (
|
||||||
|
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
type text NOT NULL,
|
||||||
|
content jsonb,
|
||||||
|
|
||||||
|
PRIMARY KEY (profile_id, created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE profile_complaint_report
|
||||||
|
ALTER COLUMN type SET STORAGE external,
|
||||||
|
ALTER COLUMN content SET STORAGE external;
|
||||||
|
|
||||||
|
ALTER TABLE profile
|
||||||
|
ADD COLUMN is_muted boolean DEFAULT false,
|
||||||
|
ADD COLUMN auth_backend text NULL;
|
||||||
|
|
||||||
|
ALTER TABLE profile
|
||||||
|
ALTER COLUMN auth_backend SET STORAGE external;
|
||||||
|
|
||||||
|
UPDATE profile
|
||||||
|
SET auth_backend = 'google'
|
||||||
|
WHERE password = '!';
|
||||||
|
|
||||||
|
UPDATE profile
|
||||||
|
SET auth_backend = 'penpot'
|
||||||
|
WHERE password != '!';
|
||||||
|
|
||||||
|
-- Table storing a permanent complaint table for register all
|
||||||
|
-- permanent bounces and spam reports (complaints) and avoid sending
|
||||||
|
-- more emails there.
|
||||||
|
CREATE TABLE global_complaint_report (
|
||||||
|
email text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
type text NOT NULL,
|
||||||
|
content jsonb,
|
||||||
|
|
||||||
|
PRIMARY KEY (email, created_at)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE global_complaint_report
|
||||||
|
ALTER COLUMN type SET STORAGE external,
|
||||||
|
ALTER COLUMN content SET STORAGE external;
|
|
@ -16,7 +16,6 @@
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.profile-initial-data :refer [create-profile-initial-data]]
|
[app.db.profile-initial-data :refer [create-profile-initial-data]]
|
||||||
[app.emails :as emails]
|
[app.emails :as emails]
|
||||||
[app.http.session :as session]
|
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.mutations.projects :as projects]
|
[app.rpc.mutations.projects :as projects]
|
||||||
[app.rpc.mutations.teams :as teams]
|
[app.rpc.mutations.teams :as teams]
|
||||||
|
@ -56,12 +55,11 @@
|
||||||
|
|
||||||
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
||||||
[{:keys [pool tokens session] :as cfg} {:keys [token] :as params}]
|
[{:keys [pool tokens session] :as cfg} {:keys [token] :as params}]
|
||||||
(when-not (:registration-enabled cfg/config)
|
(when-not (cfg/get :registration-enabled)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :registration-disabled))
|
:code :registration-disabled))
|
||||||
|
|
||||||
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
|
(when-not (email-domain-in-whitelist? (cfg/get :registration-domain-whitelist) (:email params))
|
||||||
(:email params))
|
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-domain-is-not-allowed))
|
:code :email-domain-is-not-allowed))
|
||||||
|
|
||||||
|
@ -95,29 +93,33 @@
|
||||||
(with-meta (assoc profile
|
(with-meta (assoc profile
|
||||||
:is-active true
|
:is-active true
|
||||||
:claims claims)
|
:claims claims)
|
||||||
{:transform-response
|
{:transform-response ((:create session) (:id profile))}))
|
||||||
(fn [request response]
|
|
||||||
(let [uagent (get-in request [:headers "user-agent"])
|
|
||||||
id (session/create! session {:profile-id (:id profile)
|
|
||||||
:user-agent uagent})]
|
|
||||||
(assoc response
|
|
||||||
:cookies (session/cookies session {:value id}))))}))
|
|
||||||
|
|
||||||
;; If no token is provided, send a verification email
|
;; If no token is provided, send a verification email
|
||||||
(let [token (tokens :generate
|
(let [vtoken (tokens :generate
|
||||||
{:iss :verify-email
|
{:iss :verify-email
|
||||||
:exp (dt/in-future "48h")
|
:exp (dt/in-future "48h")
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:email (:email profile)})]
|
:email (:email profile)})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
|
;; Don't allow proceed in register page if the email is
|
||||||
|
;; already reported as permanent bounced
|
||||||
|
(when (emails/has-bounce-reports? conn (:email profile))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email has one or many bounces reported"))
|
||||||
|
|
||||||
(emails/send! conn emails/register
|
(emails/send! conn emails/register
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
:token token})
|
:token vtoken
|
||||||
|
:extra-data ptoken})
|
||||||
|
|
||||||
profile)))))
|
profile)))))
|
||||||
|
|
||||||
|
|
||||||
(defn email-domain-in-whitelist?
|
(defn email-domain-in-whitelist?
|
||||||
"Returns true if email's domain is in the given whitelist or if given
|
"Returns true if email's domain is in the given whitelist or if given
|
||||||
whitelist is an empty string."
|
whitelist is an empty string."
|
||||||
|
@ -162,8 +164,8 @@
|
||||||
(defn- create-profile
|
(defn- create-profile
|
||||||
"Create the profile entry on the database with limited input
|
"Create the profile entry on the database with limited input
|
||||||
filling all the other fields with defaults."
|
filling all the other fields with defaults."
|
||||||
[conn {:keys [id fullname email password demo? props is-active]
|
[conn {:keys [id fullname email password demo? props is-active is-muted]
|
||||||
:or {is-active false}
|
:or {is-active false is-muted false}
|
||||||
:as params}]
|
:as params}]
|
||||||
(let [id (or id (uuid/next))
|
(let [id (or id (uuid/next))
|
||||||
demo? (if (boolean? demo?) demo? false)
|
demo? (if (boolean? demo?) demo? false)
|
||||||
|
@ -175,9 +177,11 @@
|
||||||
{:id id
|
{:id id
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:email (str/lower email)
|
:email (str/lower email)
|
||||||
|
:auth-backend "penpot"
|
||||||
:password password
|
:password password
|
||||||
:props props
|
:props props
|
||||||
:is-active active?
|
:is-active active?
|
||||||
|
:is-muted is-muted
|
||||||
:is-demo demo?})
|
:is-demo demo?})
|
||||||
(update :props db/decode-transit-pgobject))
|
(update :props db/decode-transit-pgobject))
|
||||||
(catch org.postgresql.util.PSQLException e
|
(catch org.postgresql.util.PSQLException e
|
||||||
|
@ -217,7 +221,7 @@
|
||||||
:opt-un [::scope]))
|
:opt-un [::scope]))
|
||||||
|
|
||||||
(sv/defmethod ::login {:auth false :rlimit :password}
|
(sv/defmethod ::login {:auth false :rlimit :password}
|
||||||
[{:keys [pool] :as cfg} {:keys [email password scope] :as params}]
|
[{:keys [pool session] :as cfg} {:keys [email password scope] :as params}]
|
||||||
(letfn [(check-password [profile password]
|
(letfn [(check-password [profile password]
|
||||||
(when (= (:password profile) "!")
|
(when (= (:password profile) "!")
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
|
@ -240,17 +244,31 @@
|
||||||
(let [prof (-> (profile/retrieve-profile-data-by-email conn email)
|
(let [prof (-> (profile/retrieve-profile-data-by-email conn email)
|
||||||
(validate-profile)
|
(validate-profile)
|
||||||
(profile/strip-private-attrs))
|
(profile/strip-private-attrs))
|
||||||
addt (profile/retrieve-additional-data conn (:id prof))]
|
addt (profile/retrieve-additional-data conn (:id prof))
|
||||||
(merge prof addt)))))
|
prof (merge prof addt)]
|
||||||
|
(with-meta prof
|
||||||
|
{:transform-response ((:create session) (:id prof))})))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- Mutation: Logout
|
||||||
|
|
||||||
|
(s/def ::logout
|
||||||
|
(s/keys :req-un [::profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::logout
|
||||||
|
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
|
||||||
|
(with-meta {}
|
||||||
|
{:transform-response (:delete session)}))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Register if not exists
|
;; --- Mutation: Register if not exists
|
||||||
|
|
||||||
|
(s/def ::backend ::us/string)
|
||||||
(s/def ::login-or-register
|
(s/def ::login-or-register
|
||||||
(s/keys :req-un [::email ::fullname]))
|
(s/keys :req-un [::email ::fullname ::backend]))
|
||||||
|
|
||||||
(sv/defmethod ::login-or-register {:auth false}
|
(sv/defmethod ::login-or-register {:auth false}
|
||||||
[{:keys [pool] :as cfg} {:keys [email fullname] :as params}]
|
[{:keys [pool] :as cfg} {:keys [email backend fullname] :as params}]
|
||||||
(letfn [(populate-additional-data [conn profile]
|
(letfn [(populate-additional-data [conn profile]
|
||||||
(let [data (profile/retrieve-additional-data conn (:id profile))]
|
(let [data (profile/retrieve-additional-data conn (:id profile))]
|
||||||
(merge profile data)))
|
(merge profile data)))
|
||||||
|
@ -260,6 +278,7 @@
|
||||||
{:id (uuid/next)
|
{:id (uuid/next)
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:email (str/lower email)
|
:email (str/lower email)
|
||||||
|
:auth-backend backend
|
||||||
:is-active true
|
:is-active true
|
||||||
:password "!"
|
:password "!"
|
||||||
:is-demo false}))
|
:is-demo false}))
|
||||||
|
@ -366,16 +385,30 @@
|
||||||
{:iss :change-email
|
{:iss :change-email
|
||||||
:exp (dt/in-future "15m")
|
:exp (dt/in-future "15m")
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:email email})]
|
:email email})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
(when (not= email (:email profile))
|
(when (not= email (:email profile))
|
||||||
(check-profile-existence! conn params))
|
(check-profile-existence! conn params))
|
||||||
|
|
||||||
|
(when-not (emails/allow-send-emails? conn profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||||
|
|
||||||
|
(when (emails/has-bounce-reports? conn email)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
(emails/send! conn emails/change-email
|
(emails/send! conn emails/change-email
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:name (:fullname profile)
|
:name (:fullname profile)
|
||||||
:pending-email email
|
:pending-email email
|
||||||
:token token})
|
:token token
|
||||||
|
:extra-data ptoken})
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(defn select-profile-for-update
|
(defn select-profile-for-update
|
||||||
|
@ -397,11 +430,15 @@
|
||||||
(assoc profile :token token)))
|
(assoc profile :token token)))
|
||||||
|
|
||||||
(send-email-notification [conn profile]
|
(send-email-notification [conn profile]
|
||||||
|
(let [ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
(emails/send! conn emails/password-recovery
|
(emails/send! conn emails/password-recovery
|
||||||
{:to (:email profile)
|
{:to (:email profile)
|
||||||
:token (:token profile)
|
:token (:token profile)
|
||||||
:name (:fullname profile)})
|
:name (:fullname profile)
|
||||||
nil)]
|
:extra-data ptoken})
|
||||||
|
nil))]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
||||||
|
@ -409,6 +446,17 @@
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :profile-not-verified
|
:code :profile-not-verified
|
||||||
:hint "the user need to validate profile before recover password"))
|
:hint "the user need to validate profile before recover password"))
|
||||||
|
|
||||||
|
(when-not (emails/allow-send-emails? conn profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||||
|
|
||||||
|
(when (emails/has-bounce-reports? conn (:email profile))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
(->> profile
|
(->> profile
|
||||||
(create-recovery-token)
|
(create-recovery-token)
|
||||||
(send-email-notification conn))))))
|
(send-email-notification conn))))))
|
||||||
|
@ -480,11 +528,7 @@
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
|
|
||||||
(with-meta {}
|
(with-meta {}
|
||||||
{:transform-response
|
{:transform-response (:delete session)})))
|
||||||
(fn [request response]
|
|
||||||
(session/delete! session request)
|
|
||||||
(assoc response
|
|
||||||
:cookies (session/cookies session {:value "" :max-age -1})))})))
|
|
||||||
|
|
||||||
(def sql:owned-teams
|
(def sql:owned-teams
|
||||||
"with owner_teams as (
|
"with owner_teams as (
|
||||||
|
|
|
@ -301,22 +301,44 @@
|
||||||
profile (db/get-by-id conn :profile profile-id)
|
profile (db/get-by-id conn :profile profile-id)
|
||||||
member (profile/retrieve-profile-data-by-email conn email)
|
member (profile/retrieve-profile-data-by-email conn email)
|
||||||
team (db/get-by-id conn :team team-id)
|
team (db/get-by-id conn :team team-id)
|
||||||
token (tokens :generate
|
itoken (tokens :generate
|
||||||
{:iss :team-invitation
|
{:iss :team-invitation
|
||||||
:exp (dt/in-future "24h")
|
:exp (dt/in-future "24h")
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:role role
|
:role role
|
||||||
:team-id team-id
|
:team-id team-id
|
||||||
:member-email (:email member email)
|
:member-email (:email member email)
|
||||||
:member-id (:id member)})]
|
:member-id (:id member)})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
(when-not (some :is-admin perms)
|
(when-not (some :is-admin perms)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :insufficient-permissions))
|
:code :insufficient-permissions))
|
||||||
|
|
||||||
|
;; First check if the current profile is allowed to send emails.
|
||||||
|
(when-not (emails/allow-send-emails? conn profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||||
|
|
||||||
|
(when (and member (not (emails/allow-send-emails? conn member)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :member-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||||
|
|
||||||
|
;; Secondly check if the invited member email is part of the
|
||||||
|
;; global spam/bounce report.
|
||||||
|
(when (emails/has-bounce-reports? conn email)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
(emails/send! conn emails/invite-to-team
|
(emails/send! conn emails/invite-to-team
|
||||||
{:to email
|
{:to email
|
||||||
:invited-by (:fullname profile)
|
:invited-by (:fullname profile)
|
||||||
:team (:name team)
|
:team (:name team)
|
||||||
:token token})
|
:token itoken
|
||||||
|
:extra-data ptoken})
|
||||||
nil)))
|
nil)))
|
||||||
|
|
|
@ -5,14 +5,13 @@
|
||||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
;; defined by the Mozilla Public License, v. 2.0.
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) 2020 UXBOX Labs SL
|
;; Copyright (c) 2020-2021 UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.rpc.mutations.verify-token
|
(ns app.rpc.mutations.verify-token
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.session :as session]
|
|
||||||
[app.rpc.mutations.teams :as teams]
|
[app.rpc.mutations.teams :as teams]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.queries.profile :as profile]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
|
@ -57,14 +56,7 @@
|
||||||
{:id (:id profile)}))
|
{:id (:id profile)}))
|
||||||
|
|
||||||
(with-meta claims
|
(with-meta claims
|
||||||
{:transform-response
|
{:transform-response ((:create session) profile-id)})))
|
||||||
(fn [request response]
|
|
||||||
(let [uagent (get-in request [:headers "user-agent"])
|
|
||||||
id (session/create! session {:profile-id profile-id
|
|
||||||
:user-agent uagent})]
|
|
||||||
(assoc response
|
|
||||||
:cookies (session/cookies session {:value id}))))})))
|
|
||||||
|
|
||||||
|
|
||||||
(defmethod process-token :auth
|
(defmethod process-token :auth
|
||||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||||
|
@ -98,11 +90,19 @@
|
||||||
(let [params (merge {:team-id team-id
|
(let [params (merge {:team-id team-id
|
||||||
:profile-id member-id}
|
:profile-id member-id}
|
||||||
(teams/role->params role))
|
(teams/role->params role))
|
||||||
claims (assoc claims :state :created)]
|
claims (assoc claims :state :created)
|
||||||
|
member (profile/retrieve-profile conn member-id)]
|
||||||
|
|
||||||
(db/insert! conn :team-profile-rel params
|
(db/insert! conn :team-profile-rel params
|
||||||
{:on-conflict-do-nothing true})
|
{:on-conflict-do-nothing true})
|
||||||
|
|
||||||
|
;; If profile is not yet verified, mark it as verified because
|
||||||
|
;; accepting an invitation link serves as verification.
|
||||||
|
(when-not (:is-active member)
|
||||||
|
(db/update! conn :profile
|
||||||
|
{:is-active true}
|
||||||
|
{:id member-id}))
|
||||||
|
|
||||||
(if (and (uuid? profile-id)
|
(if (and (uuid? profile-id)
|
||||||
(= member-id profile-id))
|
(= member-id profile-id))
|
||||||
;; If the current session is already matches the invited
|
;; If the current session is already matches the invited
|
||||||
|
@ -116,13 +116,7 @@
|
||||||
;; the user clicking the link he already has access to the
|
;; the user clicking the link he already has access to the
|
||||||
;; email account.
|
;; email account.
|
||||||
(with-meta claims
|
(with-meta claims
|
||||||
{:transform-response
|
{:transform-response ((:create session) member-id)})))
|
||||||
(fn [request response]
|
|
||||||
(let [uagent (get-in request [:headers "user-agent"])
|
|
||||||
id (session/create! session {:profile-id member-id
|
|
||||||
:user-agent uagent})]
|
|
||||||
(assoc response
|
|
||||||
:cookies (session/cookies session {:value id}))))})))
|
|
||||||
|
|
||||||
;; In this case, we wait until frontend app redirect user to
|
;; In this case, we wait until frontend app redirect user to
|
||||||
;; registeration page, the user is correctly registered and the
|
;; registeration page, the user is correctly registered and the
|
||||||
|
|
|
@ -60,11 +60,25 @@
|
||||||
(defmethod ig/pre-init-spec ::tokens [_]
|
(defmethod ig/pre-init-spec ::tokens [_]
|
||||||
(s/keys :req-un [::sprops]))
|
(s/keys :req-un [::sprops]))
|
||||||
|
|
||||||
|
(defn- generate-predefined
|
||||||
|
[cfg {:keys [iss profile-id] :as params}]
|
||||||
|
(case iss
|
||||||
|
:profile-identity
|
||||||
|
(do
|
||||||
|
(us/verify uuid? profile-id)
|
||||||
|
(generate cfg (assoc params
|
||||||
|
:exp (dt/in-future {:days 30}))))
|
||||||
|
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :not-implemented
|
||||||
|
:hint "no predefined token")))
|
||||||
|
|
||||||
(defmethod ig/init-key ::tokens
|
(defmethod ig/init-key ::tokens
|
||||||
[_ {:keys [sprops] :as cfg}]
|
[_ {:keys [sprops] :as cfg}]
|
||||||
(let [secret (derive-tokens-secret (:secret-key sprops))
|
(let [secret (derive-tokens-secret (:secret-key sprops))
|
||||||
cfg (assoc cfg ::secret secret)]
|
cfg (assoc cfg ::secret secret)]
|
||||||
(fn [action params]
|
(fn [action params]
|
||||||
(case action
|
(case action
|
||||||
|
:generate-predefined (generate-predefined cfg params)
|
||||||
:verify (verify cfg params)
|
:verify (verify cfg params)
|
||||||
:generate (generate cfg params)))))
|
:generate (generate cfg params)))))
|
||||||
|
|
|
@ -31,15 +31,24 @@
|
||||||
[environ.core :refer [env]]
|
[environ.core :refer [env]]
|
||||||
[expound.alpha :as expound]
|
[expound.alpha :as expound]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[mockery.core :as mk]
|
||||||
[promesa.core :as p])
|
[promesa.core :as p])
|
||||||
(:import org.postgresql.ds.PGSimpleDataSource))
|
(:import org.postgresql.ds.PGSimpleDataSource))
|
||||||
|
|
||||||
(def ^:dynamic *system* nil)
|
(def ^:dynamic *system* nil)
|
||||||
(def ^:dynamic *pool* nil)
|
(def ^:dynamic *pool* nil)
|
||||||
|
|
||||||
|
(def config
|
||||||
|
(merge {:redis-uri "redis://redis/1"
|
||||||
|
:database-uri "postgresql://postgres/penpot_test"
|
||||||
|
:storage-fs-directory "/tmp/app/storage"
|
||||||
|
:migrations-verbose false}
|
||||||
|
cfg/config))
|
||||||
|
|
||||||
|
|
||||||
(defn state-init
|
(defn state-init
|
||||||
[next]
|
[next]
|
||||||
(let [config (-> (main/build-system-config cfg/test-config)
|
(let [config (-> (main/build-system-config config)
|
||||||
(dissoc :app.srepl/server
|
(dissoc :app.srepl/server
|
||||||
:app.http/server
|
:app.http/server
|
||||||
:app.http/router
|
:app.http/router
|
||||||
|
@ -300,3 +309,31 @@
|
||||||
(defn sleep
|
(defn sleep
|
||||||
[ms]
|
[ms]
|
||||||
(Thread/sleep ms))
|
(Thread/sleep ms))
|
||||||
|
|
||||||
|
(defn mock-config-get-with
|
||||||
|
"Helper for mock app.config/get"
|
||||||
|
[data]
|
||||||
|
(fn
|
||||||
|
([key] (get (merge config data) key))
|
||||||
|
([key default] (get (merge config data) key default))))
|
||||||
|
|
||||||
|
(defn create-complaint-for
|
||||||
|
[conn {:keys [id created-at type]}]
|
||||||
|
(db/insert! conn :profile-complaint-report
|
||||||
|
{:profile-id id
|
||||||
|
:created-at (or created-at (dt/now))
|
||||||
|
:type (name type)
|
||||||
|
:content (db/tjson {})}))
|
||||||
|
|
||||||
|
(defn create-global-complaint-for
|
||||||
|
[conn {:keys [email type created-at]}]
|
||||||
|
(db/insert! conn :global-complaint-report
|
||||||
|
{:email email
|
||||||
|
:type (name type)
|
||||||
|
:created-at (or created-at (dt/now))
|
||||||
|
:content (db/tjson {})}))
|
||||||
|
|
||||||
|
|
||||||
|
(defn reset-mock!
|
||||||
|
[m]
|
||||||
|
(reset! m @(mk/make-mock {})))
|
||||||
|
|
316
backend/tests/app/tests/test_bounces_handling.clj
Normal file
316
backend/tests/app/tests/test_bounces_handling.clj
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2021 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.tests.test-bounces-handling
|
||||||
|
(:require
|
||||||
|
[clojure.pprint :refer [pprint]]
|
||||||
|
[app.http.awsns :as awsns]
|
||||||
|
[app.emails :as emails]
|
||||||
|
[app.tests.helpers :as th]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[mockery.core :refer [with-mocks]]
|
||||||
|
[clojure.test :as t]))
|
||||||
|
|
||||||
|
(t/use-fixtures :once th/state-init)
|
||||||
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
|
;; (with-mocks [mock {:target 'app.tasks/submit! :return nil}]
|
||||||
|
;; Right now we have many different scenarios what can cause a
|
||||||
|
;; bounce/complain report.
|
||||||
|
|
||||||
|
(defn- decode-row
|
||||||
|
[{:keys [content] :as row}]
|
||||||
|
(cond-> row
|
||||||
|
(db/pgobject? content)
|
||||||
|
(assoc :content (db/decode-transit-pgobject content))))
|
||||||
|
|
||||||
|
(defn bounce-report
|
||||||
|
[{:keys [token email] :or {email "user@example.com"}}]
|
||||||
|
{"notificationType" "Bounce",
|
||||||
|
"bounce" {"feedbackId""010701776d7dd251-c08d280d-9f47-41aa-b959-0094fec779d9-000000",
|
||||||
|
"bounceType" "Permanent",
|
||||||
|
"bounceSubType" "General",
|
||||||
|
"bouncedRecipients" [{"emailAddress" email,
|
||||||
|
"action" "failed",
|
||||||
|
"status" "5.1.1",
|
||||||
|
"diagnosticCode" "smtp; 550 5.1.1 user unknown"}]
|
||||||
|
"timestamp" "2021-02-04T14:41:38.000Z",
|
||||||
|
"remoteMtaIp" "22.22.22.22",
|
||||||
|
"reportingMTA" "dsn; b224-13.smtp-out.eu-central-1.amazonses.com"}
|
||||||
|
"mail" {"timestamp" "2021-02-04T14:41:37.020Z",
|
||||||
|
"source" "no-reply@penpot.app",
|
||||||
|
"sourceArn" "arn:aws:ses:eu-central-1:1111111111:identity/penpot.app",
|
||||||
|
"sourceIp" "22.22.22.22",
|
||||||
|
"sendingAccountId" "1111111111",
|
||||||
|
"messageId" "010701776d7dccfc-3c0094e7-01d7-458d-8100-893320186028-000000",
|
||||||
|
"destination" [email],
|
||||||
|
"headersTruncated" false,
|
||||||
|
"headers" [{"name" "Received","value" "from app-pre"},
|
||||||
|
{"name" "Date","value" "Thu, 4 Feb 2021 14:41:36 +0000 (UTC)"},
|
||||||
|
{"name" "From","value" "Penpot <no-reply@penpot.app>"},
|
||||||
|
{"name" "Reply-To","value" "Penpot <no-reply@penpot.app>"},
|
||||||
|
{"name" "To","value" email},
|
||||||
|
{"name" "Message-ID","value" "<2054501.5.1612449696846@penpot.app>"},
|
||||||
|
{"name" "Subject","value" "test"},
|
||||||
|
{"name" "MIME-Version","value" "1.0"},
|
||||||
|
{"name" "Content-Type","value" "multipart/mixed; boundary=\"----=_Part_3_1150363050.1612449696845\""},
|
||||||
|
{"name" "X-Penpot-Data","value" token}],
|
||||||
|
"commonHeaders" {"from" ["Penpot <no-reply@penpot.app>"],
|
||||||
|
"replyTo" ["Penpot <no-reply@penpot.app>"],
|
||||||
|
"date" "Thu, 4 Feb 2021 14:41:36 +0000 (UTC)",
|
||||||
|
"to" [email],
|
||||||
|
"messageId" "<2054501.5.1612449696846@penpot.app>",
|
||||||
|
"subject" "test"}}})
|
||||||
|
|
||||||
|
|
||||||
|
(defn complaint-report
|
||||||
|
[{:keys [token email] :or {email "user@example.com"}}]
|
||||||
|
{"notificationType" "Complaint",
|
||||||
|
"complaint" {"feedbackId" "0107017771528618-dcf4d61f-c889-4c8b-a6ff-6f0b6553b837-000000",
|
||||||
|
"complaintSubType" nil,
|
||||||
|
"complainedRecipients" [{"emailAddress" email}],
|
||||||
|
"timestamp" "2021-02-05T08:32:49.000Z",
|
||||||
|
"userAgent" "Yahoo!-Mail-Feedback/2.0",
|
||||||
|
"complaintFeedbackType" "abuse",
|
||||||
|
"arrivalDate" "2021-02-05T08:31:15.000Z"},
|
||||||
|
"mail" {"timestamp" "2021-02-05T08:31:13.715Z",
|
||||||
|
"source" "no-reply@penpot.app",
|
||||||
|
"sourceArn" "arn:aws:ses:eu-central-1:111111111:identity/penpot.app",
|
||||||
|
"sourceIp" "22.22.22.22",
|
||||||
|
"sendingAccountId" "11111111111",
|
||||||
|
"messageId" "0107017771510f33-a0696d28-859c-4f08-9211-8392d1b5c226-000000",
|
||||||
|
"destination" ["user@yahoo.com"],
|
||||||
|
"headersTruncated" false,
|
||||||
|
"headers" [{"name" "Received","value" "from smtp"},
|
||||||
|
{"name" "Date","value" "Fri, 5 Feb 2021 08:31:13 +0000 (UTC)"},
|
||||||
|
{"name" "From","value" "Penpot <no-reply@penpot.app>"},
|
||||||
|
{"name" "Reply-To","value" "Penpot <no-reply@penpot.app>"},
|
||||||
|
{"name" "To","value" email},
|
||||||
|
{"name" "Message-ID","value" "<1833063698.279.1612513873536@penpot.app>"},
|
||||||
|
{"name" "Subject","value" "Verify email."},
|
||||||
|
{"name" "MIME-Version","value" "1.0"},
|
||||||
|
{"name" "Content-Type","value" "multipart/mixed; boundary=\"----=_Part_276_1174403980.1612513873535\""},
|
||||||
|
{"name" "X-Penpot-Data","value" token}],
|
||||||
|
"commonHeaders" {"from" ["Penpot <no-reply@penpot.app>"],
|
||||||
|
"replyTo" ["Penpot <no-reply@penpot.app>"],
|
||||||
|
"date" "Fri, 5 Feb 2021 08:31:13 +0000 (UTC)",
|
||||||
|
"to" [email],
|
||||||
|
"messageId" "<1833063698.279.1612513873536@penpot.app>",
|
||||||
|
"subject" "Verify email."}}})
|
||||||
|
|
||||||
|
(t/deftest test-parse-bounce-report
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
cfg {:tokens tokens}
|
||||||
|
report (bounce-report {:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
result (#'awsns/parse-notification cfg report)]
|
||||||
|
;; (pprint result)
|
||||||
|
|
||||||
|
(t/is (= "bounce" (:type result)))
|
||||||
|
(t/is (= "permanent" (:kind result)))
|
||||||
|
(t/is (= "general" (:category result)))
|
||||||
|
(t/is (= ["user@example.com"] (mapv :email (:recipients result))))
|
||||||
|
(t/is (= (:id profile) (:profile-id result)))
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest test-parse-complaint-report
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
cfg {:tokens tokens}
|
||||||
|
report (complaint-report {:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
result (#'awsns/parse-notification cfg report)]
|
||||||
|
;; (pprint result)
|
||||||
|
(t/is (= "complaint" (:type result)))
|
||||||
|
(t/is (= "abuse" (:kind result)))
|
||||||
|
(t/is (= nil (:category result)))
|
||||||
|
(t/is (= ["user@example.com"] (into [] (:recipients result))))
|
||||||
|
(t/is (= (:id profile) (:profile-id result)))
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest test-parse-complaint-report-without-token
|
||||||
|
(let [tokens (:app.tokens/tokens th/*system*)
|
||||||
|
cfg {:tokens tokens}
|
||||||
|
report (complaint-report {:token ""})
|
||||||
|
result (#'awsns/parse-notification cfg report)]
|
||||||
|
(t/is (= "complaint" (:type result)))
|
||||||
|
(t/is (= "abuse" (:kind result)))
|
||||||
|
(t/is (= nil (:category result)))
|
||||||
|
(t/is (= ["user@example.com"] (into [] (:recipients result))))
|
||||||
|
(t/is (= nil (:profile-id result)))
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest test-process-bounce-report
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
cfg {:tokens tokens :pool pool}
|
||||||
|
report (bounce-report {:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
report (#'awsns/parse-notification cfg report)]
|
||||||
|
|
||||||
|
(#'awsns/process-report cfg report)
|
||||||
|
|
||||||
|
(let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
|
||||||
|
(mapv decode-row))]
|
||||||
|
(t/is (= 1 (count rows)))
|
||||||
|
(t/is (= "bounce" (get-in rows [0 :type])))
|
||||||
|
(t/is (= "2021-02-04T14:41:38.000Z" (get-in rows [0 :content :timestamp]))))
|
||||||
|
|
||||||
|
(let [rows (->> (db/query pool :global-complaint-report :all)
|
||||||
|
(mapv decode-row))]
|
||||||
|
(t/is (= 1 (count rows)))
|
||||||
|
(t/is (= "bounce" (get-in rows [0 :type])))
|
||||||
|
(t/is (= "user@example.com" (get-in rows [0 :email]))))
|
||||||
|
|
||||||
|
(let [prof (db/get-by-id pool :profile (:id profile))]
|
||||||
|
(t/is (false? (:is-muted prof))))
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest test-process-complaint-report
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
cfg {:tokens tokens :pool pool}
|
||||||
|
report (complaint-report {:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
report (#'awsns/parse-notification cfg report)]
|
||||||
|
|
||||||
|
(#'awsns/process-report cfg report)
|
||||||
|
|
||||||
|
(let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
|
||||||
|
(mapv decode-row))]
|
||||||
|
(t/is (= 1 (count rows)))
|
||||||
|
(t/is (= "complaint" (get-in rows [0 :type])))
|
||||||
|
(t/is (= "2021-02-05T08:31:15.000Z" (get-in rows [0 :content :timestamp]))))
|
||||||
|
|
||||||
|
|
||||||
|
(let [rows (->> (db/query pool :global-complaint-report :all)
|
||||||
|
(mapv decode-row))]
|
||||||
|
(t/is (= 1 (count rows)))
|
||||||
|
(t/is (= "complaint" (get-in rows [0 :type])))
|
||||||
|
(t/is (= "user@example.com" (get-in rows [0 :email]))))
|
||||||
|
|
||||||
|
|
||||||
|
(let [prof (db/get-by-id pool :profile (:id profile))]
|
||||||
|
(t/is (false? (:is-muted prof))))
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest test-process-bounce-report-to-self
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
cfg {:tokens tokens :pool pool}
|
||||||
|
report (bounce-report {:email (:email profile)
|
||||||
|
:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
report (#'awsns/parse-notification cfg report)]
|
||||||
|
|
||||||
|
(#'awsns/process-report cfg report)
|
||||||
|
|
||||||
|
(let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
|
||||||
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
|
(let [rows (db/query pool :global-complaint-report :all)]
|
||||||
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
|
(let [prof (db/get-by-id pool :profile (:id profile))]
|
||||||
|
(t/is (true? (:is-muted prof))))))
|
||||||
|
|
||||||
|
(t/deftest test-process-complaint-report-to-self
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
tokens (:app.tokens/tokens th/*system*)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
cfg {:tokens tokens :pool pool}
|
||||||
|
report (complaint-report {:email (:email profile)
|
||||||
|
:token (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})})
|
||||||
|
report (#'awsns/parse-notification cfg report)]
|
||||||
|
|
||||||
|
(#'awsns/process-report cfg report)
|
||||||
|
|
||||||
|
(let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
|
||||||
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
|
(let [rows (db/query pool :global-complaint-report :all)]
|
||||||
|
(t/is (= 1 (count rows))))
|
||||||
|
|
||||||
|
(let [prof (db/get-by-id pool :profile (:id profile))]
|
||||||
|
(t/is (true? (:is-muted prof))))))
|
||||||
|
|
||||||
|
(t/deftest test-allow-send-messages-predicate-with-bounces
|
||||||
|
(with-mocks [mock {:target 'app.config/get
|
||||||
|
:return (th/mock-config-get-with
|
||||||
|
{:profile-bounce-threshold 3
|
||||||
|
:profile-complaint-threshold 2})}]
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)]
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||||
|
|
||||||
|
(t/is (true? (emails/allow-send-emails? pool profile)))
|
||||||
|
(t/is (= 4 (:call-count (deref mock))))
|
||||||
|
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||||
|
(t/is (false? (emails/allow-send-emails? pool profile))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest test-allow-send-messages-predicate-with-complaints
|
||||||
|
(with-mocks [mock {:target 'app.config/get
|
||||||
|
:return (th/mock-config-get-with
|
||||||
|
{:profile-bounce-threshold 3
|
||||||
|
:profile-complaint-threshold 2})}]
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)]
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile) :created-at (dt/in-past {:days 8})})
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||||
|
(th/create-complaint-for pool {:type :bounce :id (:id profile)})
|
||||||
|
(th/create-complaint-for pool {:type :complaint :id (:id profile)})
|
||||||
|
|
||||||
|
(t/is (true? (emails/allow-send-emails? pool profile)))
|
||||||
|
(t/is (= 4 (:call-count (deref mock))))
|
||||||
|
|
||||||
|
(th/create-complaint-for pool {:type :complaint :id (:id profile)})
|
||||||
|
(t/is (false? (emails/allow-send-emails? pool profile))))))
|
||||||
|
|
||||||
|
(t/deftest test-has-complaint-reports-predicate
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)]
|
||||||
|
|
||||||
|
(t/is (false? (emails/has-complaint-reports? pool (:email profile))))
|
||||||
|
|
||||||
|
(th/create-global-complaint-for pool {:type :bounce :email (:email profile)})
|
||||||
|
(t/is (false? (emails/has-complaint-reports? pool (:email profile))))
|
||||||
|
|
||||||
|
(th/create-global-complaint-for pool {:type :complaint :email (:email profile)})
|
||||||
|
(t/is (true? (emails/has-complaint-reports? pool (:email profile))))))
|
||||||
|
|
||||||
|
(t/deftest test-has-bounce-reports-predicate
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)]
|
||||||
|
|
||||||
|
(t/is (false? (emails/has-bounce-reports? pool (:email profile))))
|
||||||
|
|
||||||
|
(th/create-global-complaint-for pool {:type :complaint :email (:email profile)})
|
||||||
|
(t/is (false? (emails/has-bounce-reports? pool (:email profile))))
|
||||||
|
|
||||||
|
(th/create-global-complaint-for pool {:type :bounce :email (:email profile)})
|
||||||
|
(t/is (true? (emails/has-bounce-reports? pool (:email profile))))))
|
|
@ -11,7 +11,6 @@
|
||||||
(:require
|
(:require
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
[mockery.core :refer [with-mock]]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as emails]
|
[app.emails :as emails]
|
||||||
[app.tests.helpers :as th]))
|
[app.tests.helpers :as th]))
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
[app.tests.helpers :as th]))
|
[app.tests.helpers :as th]))
|
||||||
|
|
||||||
|
;; TODO: profile deletion with teams
|
||||||
|
;; TODO: profile deletion with owner teams
|
||||||
|
|
||||||
(t/use-fixtures :once th/state-init)
|
(t/use-fixtures :once th/state-init)
|
||||||
(t/use-fixtures :each th/database-reset)
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
|
@ -187,7 +190,175 @@
|
||||||
(t/testing "not allowed email domain"
|
(t/testing "not allowed email domain"
|
||||||
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
||||||
|
|
||||||
;; TODO: profile deletion with teams
|
(t/deftest test-register-when-registration-disabled
|
||||||
;; TODO: profile deletion with owner teams
|
(with-mocks [mock {:target 'app.config/get
|
||||||
;; TODO: profile registration
|
:return (th/mock-config-get-with
|
||||||
;; TODO: profile password recovery
|
{:registration-enabled false})}]
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"
|
||||||
|
:fullname "foobar"}
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)
|
||||||
|
edata (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type edata) :restriction))
|
||||||
|
(t/is (= (:code edata) :registration-disabled)))))
|
||||||
|
|
||||||
|
(t/deftest test-register-existing-profile
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
data {::th/type :register-profile
|
||||||
|
:email (:email profile)
|
||||||
|
:password "foobar"
|
||||||
|
:fullname "foobar"}
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)
|
||||||
|
edata (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type edata) :validation))
|
||||||
|
(t/is (= (:code edata) :email-already-exists))))
|
||||||
|
|
||||||
|
(t/deftest test-register-profile
|
||||||
|
(with-mocks [mock {:target 'app.emails/send!
|
||||||
|
:return nil}]
|
||||||
|
(let [pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"
|
||||||
|
:fullname "foobar"}
|
||||||
|
out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(let [mock (deref mock)
|
||||||
|
[_ _ params] (:call-args mock)]
|
||||||
|
;; (clojure.pprint/pprint params)
|
||||||
|
(t/is (:called? mock))
|
||||||
|
(t/is (= (:email data) (:to params)))
|
||||||
|
(t/is (contains? params :extra-data))
|
||||||
|
(t/is (contains? params :token)))
|
||||||
|
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (false? (:is-demo result)))
|
||||||
|
(t/is (= (:email data) (:email result)))
|
||||||
|
(t/is (= "penpot" (:auth-backend result)))
|
||||||
|
(t/is (= "foobar" (:fullname result)))
|
||||||
|
(t/is (not (contains? result :password)))))))
|
||||||
|
|
||||||
|
(t/deftest test-register-profile-with-bounced-email
|
||||||
|
(with-mocks [mock {:target 'app.emails/send!
|
||||||
|
:return nil}]
|
||||||
|
(let [pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"
|
||||||
|
:fullname "foobar"}
|
||||||
|
_ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||||
|
out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
(let [mock (deref mock)]
|
||||||
|
(t/is (false? (:called? mock))))
|
||||||
|
|
||||||
|
(let [error (:error out)
|
||||||
|
edata (ex-data error)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (= (:type edata) :validation))
|
||||||
|
(t/is (= (:code edata) :email-has-permanent-bounces))))))
|
||||||
|
|
||||||
|
(t/deftest test-register-profile-with-complained-email
|
||||||
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
|
(let [pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"
|
||||||
|
:fullname "foobar"}
|
||||||
|
_ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||||
|
out (th/mutation! data)]
|
||||||
|
|
||||||
|
(let [mock (deref mock)]
|
||||||
|
(t/is (true? (:called? mock))))
|
||||||
|
|
||||||
|
(let [result (:result out)]
|
||||||
|
(t/is (= (:email data) (:email result)))))))
|
||||||
|
|
||||||
|
(t/deftest test-email-change-request
|
||||||
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
|
(let [profile (th/create-profile* 1)
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :request-email-change
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email "user1@example.com"}]
|
||||||
|
|
||||||
|
;; without complaints
|
||||||
|
(let [out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(let [mock (deref mock)]
|
||||||
|
(t/is (= 1 (:call-count mock)))
|
||||||
|
(t/is (true? (:called? mock)))))
|
||||||
|
|
||||||
|
;; with complaints
|
||||||
|
(th/create-global-complaint-for pool {:type :complaint :email (:email data)})
|
||||||
|
(let [out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 2 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; with bounces
|
||||||
|
(th/create-global-complaint-for pool {:type :bounce :email (:email data)})
|
||||||
|
(let [out (th/mutation! data)
|
||||||
|
error (:error out)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
||||||
|
(t/is (= 2 (:call-count (deref mock))))))))
|
||||||
|
|
||||||
|
(t/deftest test-request-profile-recovery
|
||||||
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
|
(let [profile1 (th/create-profile* 1)
|
||||||
|
profile2 (th/create-profile* 2 {:is-active true})
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :request-profile-recovery}]
|
||||||
|
|
||||||
|
;; with invalid email
|
||||||
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 0 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; with valid email inactive user
|
||||||
|
(let [data (assoc data :email (:email profile1))
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)]
|
||||||
|
(t/is (= 0 (:call-count (deref mock))))
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :profile-not-verified)))
|
||||||
|
|
||||||
|
;; with valid email and active user
|
||||||
|
(let [data (assoc data :email (:email profile2))
|
||||||
|
out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; with valid email and active user with global complaints
|
||||||
|
(th/create-global-complaint-for pool {:type :complaint :email (:email profile2)})
|
||||||
|
(let [data (assoc data :email (:email profile2))
|
||||||
|
out (th/mutation! data)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 2 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; with valid email and active user with global bounce
|
||||||
|
(th/create-global-complaint-for pool {:type :bounce :email (:email profile2)})
|
||||||
|
(let [data (assoc data :email (:email profile2))
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (= 2 (:call-count (deref mock))))
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))
|
||||||
|
|
||||||
|
)))
|
||||||
|
|
88
backend/tests/app/tests/test_services_teams.clj
Normal file
88
backend/tests/app/tests/test_services_teams.clj
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
;; 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/.
|
||||||
|
;;
|
||||||
|
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
;; defined by the Mozilla Public License, v. 2.0.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) 2020 UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.tests.test-services-teams
|
||||||
|
(:require
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.http :as http]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.tests.helpers :as th]
|
||||||
|
[mockery.core :refer [with-mocks]]
|
||||||
|
[clojure.test :as t]
|
||||||
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
(t/use-fixtures :once th/state-init)
|
||||||
|
(t/use-fixtures :each th/database-reset)
|
||||||
|
|
||||||
|
(t/deftest test-invite-team-member
|
||||||
|
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
||||||
|
(let [profile1 (th/create-profile* 1 {:is-active true})
|
||||||
|
profile2 (th/create-profile* 2 {:is-active true})
|
||||||
|
profile3 (th/create-profile* 3 {:is-active true :is-muted true})
|
||||||
|
|
||||||
|
team (th/create-team* 1 {:profile-id (:id profile1)})
|
||||||
|
|
||||||
|
pool (:app.db/pool th/*system*)
|
||||||
|
data {::th/type :invite-team-member
|
||||||
|
:team-id (:id team)
|
||||||
|
:role :editor
|
||||||
|
:profile-id (:id profile1)}]
|
||||||
|
|
||||||
|
;; (th/print-result! out)
|
||||||
|
|
||||||
|
;; invite external user without complaints
|
||||||
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; invite internal user without complaints
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(let [data (assoc data :email (:email profile2))
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; invite user with complaint
|
||||||
|
(th/create-global-complaint-for pool {:type :complaint :email "foo@bar.com"})
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(t/is (nil? (:result out)))
|
||||||
|
(t/is (= 1 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; invite user with bounce
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(th/create-global-complaint-for pool {:type :bounce :email "foo@bar.com"})
|
||||||
|
(let [data (assoc data :email "foo@bar.com")
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)]
|
||||||
|
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces))
|
||||||
|
(t/is (= 0 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
;; invite internal user that is muted
|
||||||
|
(th/reset-mock! mock)
|
||||||
|
(let [data (assoc data :email (:email profile3))
|
||||||
|
out (th/mutation! data)
|
||||||
|
error (:error out)]
|
||||||
|
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :member-is-muted))
|
||||||
|
(t/is (= 0 (:call-count (deref mock)))))
|
||||||
|
|
||||||
|
)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,10 @@ http {
|
||||||
proxy_pass http://127.0.0.1:6060/api;
|
proxy_pass http://127.0.0.1:6060/api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /webhooks {
|
||||||
|
proxy_pass http://127.0.0.1:6060/webhooks;
|
||||||
|
}
|
||||||
|
|
||||||
location /dbg {
|
location /dbg {
|
||||||
proxy_pass http://127.0.0.1:6060/dbg;
|
proxy_pass http://127.0.0.1:6060/dbg;
|
||||||
}
|
}
|
||||||
|
|
|
@ -836,6 +836,28 @@
|
||||||
"es" : "Autenticación con google esta dehabilitada en el servidor"
|
"es" : "Autenticación con google esta dehabilitada en el servidor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"errors.profile-is-muted" : {
|
||||||
|
"translations" : {
|
||||||
|
"en" : "Your profile has emails muted (spam reports or high bounces).",
|
||||||
|
"es" : "Tu perfil tiene los emails silenciados (por reportes de spam o alto indice de rebote)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"errors.member-is-muted" : {
|
||||||
|
"translations" : {
|
||||||
|
"en" : "The profile you inviting has emails muted (spam reports or high bounces).",
|
||||||
|
"es" : "El perfil que esta invitando tiene los emails silenciados (por reportes de spam o alto indice de rebote)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"errors.email-has-permanent-bounces" : {
|
||||||
|
"translations" : {
|
||||||
|
"en" : "The email «%s» has many permanent bounce reports.",
|
||||||
|
"es" : "El email «%s» tiene varios reportes de rebote permanente."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"errors.auth.unauthorized" : {
|
"errors.auth.unauthorized" : {
|
||||||
"used-in" : [ "src/app/main/ui/auth/login.cljs:89" ],
|
"used-in" : [ "src/app/main/ui/auth/login.cljs:89" ],
|
||||||
"translations" : {
|
"translations" : {
|
||||||
|
|
|
@ -122,18 +122,6 @@
|
||||||
(seq params))
|
(seq params))
|
||||||
(send-mutation! id form)))
|
(send-mutation! id form)))
|
||||||
|
|
||||||
(defmethod mutation :login
|
|
||||||
[id params]
|
|
||||||
(let [uri (str cfg/public-uri "/api/login")]
|
|
||||||
(->> (http/send! {:method :post :uri uri :body params})
|
|
||||||
(rx/mapcat handle-response))))
|
|
||||||
|
|
||||||
(defmethod mutation :logout
|
|
||||||
[id params]
|
|
||||||
(let [uri (str cfg/public-uri "/api/logout")]
|
|
||||||
(->> (http/send! {:method :post :uri uri :body params})
|
|
||||||
(rx/mapcat handle-response))))
|
|
||||||
|
|
||||||
(defmethod mutation :login-with-ldap
|
(defmethod mutation :login-with-ldap
|
||||||
[id params]
|
[id params]
|
||||||
(let [uri (str cfg/public-uri "/api/login-ldap")]
|
(let [uri (str cfg/public-uri "/api/login-ldap")]
|
||||||
|
|
|
@ -63,7 +63,6 @@
|
||||||
|
|
||||||
on-error
|
on-error
|
||||||
(fn [form event]
|
(fn [form event]
|
||||||
(js/console.log error?)
|
|
||||||
(reset! error? true))
|
(reset! error? true))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
|
@ -107,8 +106,7 @@
|
||||||
:help-icon i/eye
|
:help-icon i/eye
|
||||||
:label (tr "auth.password")}]]
|
:label (tr "auth.password")}]]
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.login-submit")
|
{:label (tr "auth.login-submit")}]
|
||||||
:on-click on-submit}]
|
|
||||||
|
|
||||||
(when cfg/login-with-ldap
|
(when cfg/login-with-ldap
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr t]]
|
[app.util.i18n :as i18n :refer [tr t]]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
|
[beicon.core :as rx]
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[beicon.core :as rx]
|
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
|
@ -28,37 +28,41 @@
|
||||||
|
|
||||||
(mf/defc recovery-form
|
(mf/defc recovery-form
|
||||||
[]
|
[]
|
||||||
(let [form (fm/use-form :spec ::recovery-request-form
|
(let [form (fm/use-form :spec ::recovery-request-form :initial {})
|
||||||
:initial {})
|
|
||||||
|
|
||||||
submitted (mf/use-state false)
|
submitted (mf/use-state false)
|
||||||
|
|
||||||
on-error
|
|
||||||
(mf/use-callback
|
|
||||||
(fn [{:keys [code] :as error}]
|
|
||||||
(reset! submitted false)
|
|
||||||
(if (= code :profile-not-verified)
|
|
||||||
(rx/of (dm/error (tr "auth.notifications.profile-not-verified")
|
|
||||||
{:timeout nil}))
|
|
||||||
|
|
||||||
(rx/throw error))))
|
|
||||||
|
|
||||||
on-success
|
on-success
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn []
|
(fn [data]
|
||||||
(reset! submitted false)
|
(reset! submitted false)
|
||||||
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
|
(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
|
||||||
(rt/nav :auth-login))))
|
(rt/nav :auth-login))))
|
||||||
|
|
||||||
|
on-error
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [data {:keys [code] :as error}]
|
||||||
|
(reset! submitted false)
|
||||||
|
(case code
|
||||||
|
:profile-not-verified
|
||||||
|
(rx/of (dm/error (tr "auth.notifications.profile-not-verified") {:timeout nil}))
|
||||||
|
|
||||||
|
:profile-is-muted
|
||||||
|
(rx/of (dm/error (tr "errors.profile-is-muted")))
|
||||||
|
|
||||||
|
:email-has-permanent-bounces
|
||||||
|
(rx/of (dm/error (tr "errors.email-has-permanent-bounces" (:email data))))
|
||||||
|
|
||||||
|
(rx/throw error))))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn []
|
(fn []
|
||||||
(reset! submitted true)
|
(reset! submitted true)
|
||||||
(->> (with-meta (:clean-data @form)
|
(let [cdata (:clean-data @form)
|
||||||
{:on-success on-success
|
params (with-meta cdata
|
||||||
:on-error on-error})
|
{:on-success #(on-success cdata %)
|
||||||
(uda/request-profile-recovery)
|
:on-error #(on-error cdata %)})]
|
||||||
(st/emit!))))]
|
(st/emit! (uda/request-profile-recovery params)))))]
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:form form}
|
:form form}
|
||||||
|
|
|
@ -64,13 +64,17 @@
|
||||||
(reset! submitted? false)
|
(reset! submitted? false)
|
||||||
(case (:code error)
|
(case (:code error)
|
||||||
:registration-disabled
|
:registration-disabled
|
||||||
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
(rx/of (dm/error (tr "errors.registration-disabled")))
|
||||||
|
|
||||||
|
:email-has-permanent-bounces
|
||||||
|
(let [email (get @form [:data :email])]
|
||||||
|
(rx/of (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
||||||
|
|
||||||
:email-already-exists
|
:email-already-exists
|
||||||
(swap! form assoc-in [:errors :email]
|
(swap! form assoc-in [:errors :email]
|
||||||
{:message "errors.email-already-exists"})
|
{:message "errors.email-already-exists"})
|
||||||
|
|
||||||
(st/emit! (dm/error (tr "errors.unexpected-error"))))))
|
(rx/throw error))))
|
||||||
|
|
||||||
on-success
|
on-success
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
|
|
|
@ -97,13 +97,34 @@
|
||||||
(st/emitf (dm/success "Invitation sent successfully")
|
(st/emitf (dm/success "Invitation sent successfully")
|
||||||
(modal/hide)))
|
(modal/hide)))
|
||||||
|
|
||||||
|
on-error
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(fn [form {:keys [type code] :as error}]
|
||||||
|
(let [email (get @form [:data :email])]
|
||||||
|
(cond
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :profile-is-muted code))
|
||||||
|
(dm/error (tr "errors.profile-is-muted"))
|
||||||
|
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :member-is-muted code))
|
||||||
|
(dm/error (tr "errors.member-is-muted"))
|
||||||
|
|
||||||
|
(and (= :validation type)
|
||||||
|
(= :email-has-permanent-bounces))
|
||||||
|
(dm/error (tr "errors.email-has-permanent-bounces" email))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(dm/error (tr "errors.generic"))))))
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps team)
|
(mf/deps team)
|
||||||
(fn [form]
|
(fn [form]
|
||||||
(let [params (:clean-data @form)
|
(let [params (:clean-data @form)
|
||||||
mdata {:on-success (partial on-success form)}]
|
mdata {:on-success (partial on-success form)
|
||||||
|
:on-error (partial on-error form)}]
|
||||||
(st/emit! (dd/invite-team-member (with-meta params mdata))))))]
|
(st/emit! (dd/invite-team-member (with-meta params mdata))))))]
|
||||||
|
|
||||||
[:div.modal.dashboard-invite-modal.form-container
|
[:div.modal.dashboard-invite-modal.form-container
|
||||||
|
|
|
@ -40,14 +40,20 @@
|
||||||
(s/keys :req-un [::email-1 ::email-2]))
|
(s/keys :req-un [::email-1 ::email-2]))
|
||||||
|
|
||||||
(defn- on-error
|
(defn- on-error
|
||||||
[form error]
|
[form {:keys [code] :as error}]
|
||||||
(cond
|
(case code
|
||||||
(= (:code error) :email-already-exists)
|
:email-already-exists
|
||||||
(swap! form (fn [data]
|
(swap! form (fn [data]
|
||||||
(let [error {:message (tr "errors.email-already-exists")}]
|
(let [error {:message (tr "errors.email-already-exists")}]
|
||||||
(assoc-in data [:errors :email-1] error))))
|
(assoc-in data [:errors :email-1] error))))
|
||||||
|
|
||||||
:else
|
:profile-is-muted
|
||||||
|
(rx/of (dm/error (tr "errors.profile-is-muted")))
|
||||||
|
|
||||||
|
:email-has-permanent-bounces
|
||||||
|
(let [email (get @form [:data email])]
|
||||||
|
(rx/of (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
||||||
|
|
||||||
(rx/throw error)))
|
(rx/throw error)))
|
||||||
|
|
||||||
(defn- on-success
|
(defn- on-success
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue