mirror of
https://github.com/penpot/penpot.git
synced 2026-07-02 04:15:26 +00:00
✨ Send warning for email about nitrate orgs with sso (#10413)
This commit is contained in:
parent
2eb9423963
commit
2a5b6a69ad
@ -195,21 +195,39 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20"
|
||||
style="display:inline-block;vertical-align:middle;">
|
||||
<tr>
|
||||
<td width="20" height="20" align="center" valign="middle"
|
||||
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
||||
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
|
||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
||||
{% if organization.initials %}{{organization.initials}}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||
{{ organization.name|abbreviate:50 }}
|
||||
<span
|
||||
style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||
{{ organization.name|abbreviate:50 }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% if organization.sso-active %}
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||
teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't
|
||||
in the directory yet.
|
||||
To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
Hello!
|
||||
|
||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”.
|
||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:50 }}”.
|
||||
|
||||
{% if organization.sso-active %}
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
{% endif %}
|
||||
|
||||
Accept invitation using this link:
|
||||
|
||||
|
||||
@ -186,10 +186,26 @@
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
|
||||
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if
|
||||
organization %}
|
||||
part of the organization “{{ organization.name|abbreviate:50 }}”{% endif %}.</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% if organization.sso-active %}
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to
|
||||
its
|
||||
teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't
|
||||
in the directory yet.
|
||||
To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle"
|
||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
@ -241,4 +257,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@ -1,6 +1,11 @@
|
||||
Hello!
|
||||
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization.name|abbreviate:50 }}"{% endif %}.
|
||||
|
||||
{% if organization.sso-active %}
|
||||
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
{% endif %}
|
||||
|
||||
Accept invitation using this link:
|
||||
|
||||
|
||||
223
backend/resources/app/email/organization-setup-sso/en.html
Normal file
223
backend/resources/app/email/organization-setup-sso/en.html
Normal file
@ -0,0 +1,223 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mj-column-px-425 {
|
||||
width: 425px !important;
|
||||
max-width: 425px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#E5E5E5;">
|
||||
<div style="background-color:#E5E5E5;">
|
||||
<!--[if mso | IE]>
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:97px;">
|
||||
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
|
||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
||||
width="97" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table
|
||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
||||
>
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
|
||||
<tr>
|
||||
|
||||
<td
|
||||
class="" style="vertical-align:top;width:600px;"
|
||||
>
|
||||
<![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||
width="100%">
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %},
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
|
||||
teams and files goes through your organization's identity provider. If you can't get in, your
|
||||
account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div
|
||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||
The Penpot team.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% include "app/email/includes/footer.html" %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -0,0 +1 @@
|
||||
“{{ organization-name|abbreviate:25 }}” has set up single sign-on (SSO) in Penpot
|
||||
@ -0,0 +1,7 @@
|
||||
Hello!
|
||||
|
||||
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files goes
|
||||
through your organization's identity provider. If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
|
||||
|
||||
|
||||
The Penpot team.
|
||||
@ -419,10 +419,19 @@
|
||||
:id ::change-email
|
||||
:schema schema:change-email))
|
||||
|
||||
(def ^:private schema:organization-data
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:initials {:optional true} [:maybe :string]]
|
||||
[:logo {:optional true} [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url {:optional true} [:maybe ::sm/uri]]
|
||||
[:sso-active {:optional true} [:maybe ::sm/boolean]]])
|
||||
|
||||
(def ^:private schema:invite-to-team
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
[:team ::sm/text]
|
||||
[:organization {:optional true} [:maybe schema:organization-data]]
|
||||
[:token ::sm/text]])
|
||||
|
||||
(def invite-to-team
|
||||
@ -431,13 +440,6 @@
|
||||
:id ::invite-to-team
|
||||
:schema schema:invite-to-team))
|
||||
|
||||
(def ^:private schema:organization-data
|
||||
[:map
|
||||
[:name ::sm/text]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]])
|
||||
|
||||
(def ^:private schema:invite-to-org
|
||||
[:map
|
||||
[:invited-by ::sm/text]
|
||||
@ -451,7 +453,16 @@
|
||||
:id ::invite-to-org
|
||||
:schema schema:invite-to-org))
|
||||
|
||||
(def ^:private schema:organization-setup-sso
|
||||
[:map
|
||||
[:user-name {:optional true} [:maybe ::sm/text]]
|
||||
[:organization-name ::sm/text]])
|
||||
|
||||
(def organization-setup-sso
|
||||
"Email when an organization set up SSO"
|
||||
(template-factory
|
||||
:id ::organization-setup-sso
|
||||
:schema schema:organization-setup-sso))
|
||||
|
||||
(def ^:private schema:renewal-notice
|
||||
[:map
|
||||
|
||||
@ -22,15 +22,26 @@
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup :as-alias setup]
|
||||
[clojure.core :as c]
|
||||
[clojure.string :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- join-path-segments
|
||||
"Build a single relative path from Nitrate URI segments, normalizing slashes."
|
||||
[segments]
|
||||
(let [path (->> segments (map str) (str/join "/"))]
|
||||
(->> (str/split path #"/")
|
||||
(remove str/blank?)
|
||||
(str/join "/"))))
|
||||
|
||||
(defn- join-base-uri
|
||||
"Join path segments to a base URI."
|
||||
[base-uri & segments]
|
||||
(apply u/join (u/ensure-path-slash base-uri) segments))
|
||||
(u/join (u/ensure-path-slash base-uri)
|
||||
(join-path-segments segments)))
|
||||
|
||||
(defn- generate-nitrate-uri
|
||||
"Joins relative path segments to the Nitrate backend URI.
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.nitrate.emails-helper :as neh]
|
||||
[app.rpc.nitrate.organization-helper :as noh]
|
||||
[app.rpc.notifications :as notifications]
|
||||
[app.tokens :as tokens]
|
||||
[app.util.services :as sv]))
|
||||
@ -394,12 +396,6 @@
|
||||
(notifications/notify-team-change cfg {:id team-id :organization {:name organization-name}} "dashboard.team-no-longer-belong-org")
|
||||
nil)
|
||||
|
||||
(def ^:private sql:get-team-invitation-emails
|
||||
"SELECT email_to
|
||||
FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
AND valid_until > now()")
|
||||
|
||||
(def ^:private sql:delete-team-external-invitations
|
||||
"DELETE FROM team_invitation
|
||||
WHERE team_id = ?
|
||||
@ -421,8 +417,7 @@
|
||||
allows-anybody (nitrate-perms/allowed? :add-anybody-to-team {:org-perms org-perms})]
|
||||
(if allows-anybody
|
||||
{:allows-anybody true :external-emails []}
|
||||
(let [invitation-emails (db/exec! conn [sql:get-team-invitation-emails team-id])
|
||||
emails (map :email-to invitation-emails)]
|
||||
(let [emails (map :email (noh/get-team-invitation-emails conn team-id))]
|
||||
(if (empty? emails)
|
||||
{:allows-anybody false :external-emails []}
|
||||
(let [emails-array (db/create-array conn "text" (vec emails))
|
||||
@ -451,7 +446,8 @@
|
||||
(assert-membership cfg profile-id organization-id)
|
||||
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-with-org (nitrate/call cfg :get-team-org {:team-id team-id})
|
||||
(let [org-member-ids-before (into #{} (nitrate/call cfg :get-org-members {:organization-id organization-id}))
|
||||
team-with-org (nitrate/call cfg :get-team-org {:team-id team-id})
|
||||
source-org-id (get-in team-with-org [:organization :id])
|
||||
source-org-perms (when source-org-id
|
||||
(nitrate/call cfg :get-org-permissions
|
||||
@ -487,26 +483,32 @@
|
||||
:profile-id profile-id})
|
||||
(ex/raise :type :validation
|
||||
:code :not-allowed
|
||||
:hint "You are not allowed to add teams in this organization")))
|
||||
:hint "You are not allowed to add teams in this organization"))
|
||||
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})]
|
||||
;; Add teammates to the org if needed
|
||||
(doseq [{member-id :profile-id} team-members
|
||||
:when (not= member-id profile-id)]
|
||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
||||
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||
new-member-ids (->> team-members
|
||||
(map :profile-id)
|
||||
(remove #{profile-id})
|
||||
(remove org-member-ids-before))]
|
||||
(doseq [member-id new-member-ids]
|
||||
(teams/initialize-user-in-nitrate-org cfg member-id organization-id)))
|
||||
|
||||
;; Api call to nitrate
|
||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})]
|
||||
;; Api call to nitrate
|
||||
(let [team (nitrate/call cfg :set-team-org {:team-id team-id :organization-id organization-id :is-default false})]
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
||||
|
||||
;; Notify connected users
|
||||
(notifications/notify-team-change cfg team "dashboard.team-belong-org"))
|
||||
;; Delete pending invitations for users who are not members of the target organization
|
||||
(let [{:keys [allows-anybody external-emails]} (get-external-invitation-info cfg team-id organization-id)]
|
||||
(when (and (not allows-anybody) (seq external-emails))
|
||||
(let [conn (::db/conn cfg)
|
||||
emails-array (db/create-array conn "text" external-emails)]
|
||||
(db/exec! conn [sql:delete-team-external-invitations team-id emails-array]))))
|
||||
|
||||
;; Delete pending invitations for users who are not members of the target organization
|
||||
(let [{:keys [allows-anybody external-emails]} (get-external-invitation-info cfg team-id organization-id)]
|
||||
(when (and (not allows-anybody) (seq external-emails))
|
||||
(let [conn (::db/conn cfg)
|
||||
emails-array (db/create-array conn "text" external-emails)]
|
||||
(db/exec! conn [sql:delete-team-external-invitations team-id emails-array])))))
|
||||
;; Send warnings via email if the org has sso
|
||||
(neh/send-organization-setup-sso-emails-for-team!
|
||||
cfg organization-id team-id org-member-ids-before)))
|
||||
|
||||
nil)
|
||||
|
||||
|
||||
@ -94,7 +94,9 @@
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo ::sm/uri]]]
|
||||
[:logo ::sm/uri]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]
|
||||
[:sso-active [:maybe ::sm/boolean]]]]
|
||||
[:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
@ -246,7 +248,7 @@
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:organization (dm/get-in team [:organization :name])
|
||||
:organization (:organization team)
|
||||
:token itoken
|
||||
:extra-data ptoken})))
|
||||
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.commands.teams-invitations :as ti]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.rpc.nitrate.emails-helper :as neh]
|
||||
[app.rpc.nitrate.organization-helper :as noh]
|
||||
[app.rpc.notifications :as notifications]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
@ -485,23 +487,6 @@ RETURNING id, deleted_at;")
|
||||
|
||||
;; API: get-org-invitations
|
||||
|
||||
(def ^:private sql:get-org-invitations
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.id,
|
||||
ti.org_id AS organization_id,
|
||||
ti.email_to AS email,
|
||||
ti.created_at AS sent_at,
|
||||
p.fullname AS name,
|
||||
p.id AS profile_id,
|
||||
p.photo_id
|
||||
FROM team_invitation AS ti
|
||||
LEFT JOIN profile AS p
|
||||
ON p.email = ti.email_to
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE ti.valid_until >= now()
|
||||
AND (ti.org_id = ? OR ti.team_id = ANY(?))
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(def ^:private schema:get-org-invitations-params
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]])
|
||||
@ -523,18 +508,13 @@ LEFT JOIN profile AS p
|
||||
::sm/params schema:get-org-invitations-params
|
||||
::sm/result schema:get-org-invitations-result}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(into []))]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(->> (db/exec! conn [sql:get-org-invitations organization-id ids-array])
|
||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
||||
(cond-> (dissoc invitation :photo-id)
|
||||
photo-id
|
||||
(assoc :photo-url (files/resolve-public-uri photo-id)))))))))))
|
||||
(->> (noh/get-org-invitations conn organization-id team-ids)
|
||||
(mapv (fn [{:keys [photo-id] :as invitation}]
|
||||
(cond-> (dissoc invitation :photo-id)
|
||||
photo-id
|
||||
(assoc :photo-url (files/resolve-public-uri photo-id))))))))))
|
||||
|
||||
|
||||
;; API: delete-org-invitations
|
||||
@ -554,12 +534,8 @@ LEFT JOIN profile AS p
|
||||
{::doc/added "2.16"
|
||||
::sm/params schema:delete-org-invitations-params}
|
||||
[cfg {:keys [organization-id email]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
clean-email (profile/clean-email email)
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(into []))]
|
||||
(let [clean-email (profile/clean-email email)
|
||||
team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-org-invitations clean-email organization-id ids-array]))))
|
||||
@ -585,9 +561,7 @@ LEFT JOIN profile AS p
|
||||
::sm/params schema:delete-all-org-invitations-params
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [organization-id]}]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})
|
||||
team-ids (->> (:teams org-summary)
|
||||
(map :id))]
|
||||
(let [team-ids (noh/get-org-team-ids cfg organization-id)]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:delete-all-org-invitations organization-id ids-array]))))
|
||||
@ -905,17 +879,19 @@ LEFT JOIN profile AS p
|
||||
owner-photo-id (assoc :owner-photo-url (files/resolve-public-uri owner-photo-id))))))))))))
|
||||
|
||||
;; ---- API: notify-org-sso-change
|
||||
|
||||
(sv/defmethod ::notify-org-sso-change
|
||||
"Nitrate notifies that an organization sso values have changed"
|
||||
{::doc/added "2.19"
|
||||
::sm/params [:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:updated-props ::sm/boolean]]
|
||||
[:updated-props ::sm/boolean]
|
||||
[:became-active ::sm/boolean]]
|
||||
::rpc/auth false}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props became-active]}]
|
||||
(when updated-props
|
||||
(rpc/invalidate-org-sso-cache-by-org! organization-id)
|
||||
(session/clear-org-sso-sessions! pool organization-id))
|
||||
(notifications/notify-organization-change-sso cfg organization-id)
|
||||
(when became-active
|
||||
(neh/send-organization-setup-sso-emails! cfg organization-id))
|
||||
nil)
|
||||
|
||||
106
backend/src/app/rpc/nitrate/emails_helper.clj
Normal file
106
backend/src/app/rpc/nitrate/emails_helper.clj
Normal file
@ -0,0 +1,106 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.rpc.nitrate.emails-helper
|
||||
"Helpers for organization SSO notification emails triggered by Nitrate integration."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as eml]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.nitrate.organization-helper :as neh]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def ^:private sql:get-profile-emails-by-ids
|
||||
"SELECT email
|
||||
FROM profile
|
||||
WHERE id = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(def ^:private sql:get-profiles-by-emails
|
||||
"SELECT id, email, fullname, is_muted
|
||||
FROM profile
|
||||
WHERE email = ANY(?)
|
||||
AND deleted_at IS NULL")
|
||||
|
||||
(defn- org-sso-active?
|
||||
"Return whether SSO is enabled for the organization."
|
||||
[cfg organization-id]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(true? (:active (nitrate/call cfg :get-org-sso {:organization-id organization-id})))))
|
||||
|
||||
(def ^:private xf:map-email (map :email))
|
||||
|
||||
(defn- recipients-by-emails
|
||||
"Build `{:email :user-name :profile}` maps for a deduplicated email list."
|
||||
[conn emails]
|
||||
(let [profiles (if (seq emails)
|
||||
(let [emails-array (db/create-array conn "text" emails)]
|
||||
(db/exec! conn [sql:get-profiles-by-emails emails-array]))
|
||||
[])
|
||||
profile-by-email (d/index-by (comp str/lower :email) profiles)]
|
||||
(map (fn [email]
|
||||
(let [profile (get profile-by-email (str/lower email))]
|
||||
{:email email
|
||||
:user-name (:fullname profile)
|
||||
:profile profile}))
|
||||
emails)))
|
||||
|
||||
(defn- send-organization-setup-sso-email!
|
||||
"Send the organization SSO setup email to a single recipient, when allowed."
|
||||
[conn organization-name {:keys [email user-name profile]}]
|
||||
(when (or (nil? profile)
|
||||
(eml/allow-send-emails? conn profile))
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/organization-setup-sso
|
||||
:public-uri (cf/get :public-uri)
|
||||
:to email
|
||||
:user-name user-name
|
||||
:organization-name organization-name})))
|
||||
|
||||
(defn- get-org-sso-notify-recipients
|
||||
"Unique org members and pending org/team invitees for SSO activation emails."
|
||||
[conn cfg organization-id org-summary]
|
||||
(let [member-ids (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||
team-ids (neh/get-org-team-ids org-summary)
|
||||
member-emails (if (seq member-ids)
|
||||
(let [ids-array (db/create-array conn "uuid" member-ids)]
|
||||
(into #{} (map :email (db/exec! conn [sql:get-profile-emails-by-ids ids-array]))))
|
||||
#{})
|
||||
invite-emails (into #{} (map :email
|
||||
(neh/get-org-invitations conn organization-id team-ids)))
|
||||
emails (into #{} (concat member-emails invite-emails))]
|
||||
(recipients-by-emails conn emails)))
|
||||
|
||||
(defn- get-team-sso-notify-recipients
|
||||
"Team members who are not in `org-member-ids`, plus pending team invitations."
|
||||
[conn team-id org-member-ids]
|
||||
(let [team-members (->> (teams/get-team-members conn team-id)
|
||||
(remove #(contains? org-member-ids (:id %))))
|
||||
invitations (neh/get-team-invitation-emails conn team-id)]
|
||||
(->> (sequence xf:map-email (concat team-members invitations))
|
||||
(recipients-by-emails conn))))
|
||||
|
||||
(defn send-organization-setup-sso-emails!
|
||||
"Notify all org members and pending org/team invitees that SSO is active."
|
||||
[cfg organization-id]
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-org-sso-notify-recipients conn cfg organization-id org-summary)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient))))))
|
||||
|
||||
(defn send-organization-setup-sso-emails-for-team!
|
||||
"Notify team members who are not in `org-member-ids-before` and pending team invitees."
|
||||
[cfg organization-id team-id org-member-ids-before]
|
||||
(when (org-sso-active? cfg organization-id)
|
||||
(let [org-summary (nitrate/call cfg :get-org-summary {:organization-id organization-id})]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(doseq [recipient (get-team-sso-notify-recipients conn team-id org-member-ids-before)]
|
||||
(send-organization-setup-sso-email! conn (:name org-summary) recipient)))))))
|
||||
60
backend/src/app/rpc/nitrate/organization_helper.clj
Normal file
60
backend/src/app/rpc/nitrate/organization_helper.clj
Normal file
@ -0,0 +1,60 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.rpc.nitrate.organization-helper
|
||||
"Shared Nitrate organization query helpers."
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.nitrate :as nitrate]))
|
||||
|
||||
(def ^:private sql:get-org-invitations
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.id,
|
||||
ti.org_id AS organization_id,
|
||||
ti.email_to AS email,
|
||||
ti.created_at AS sent_at,
|
||||
p.fullname AS name,
|
||||
p.id AS profile_id,
|
||||
p.photo_id
|
||||
FROM team_invitation AS ti
|
||||
LEFT JOIN profile AS p
|
||||
ON p.email = ti.email_to
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE ti.valid_until >= now()
|
||||
AND (ti.org_id = ? OR ti.team_id = ANY(?))
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(def ^:private sql:get-team-invitation-emails
|
||||
"SELECT DISTINCT ON (email_to)
|
||||
ti.email_to AS email
|
||||
FROM team_invitation AS ti
|
||||
WHERE ti.team_id = ?
|
||||
AND ti.valid_until >= now()
|
||||
ORDER BY ti.email_to, ti.valid_until DESC, ti.created_at DESC;")
|
||||
|
||||
(defn get-org-team-ids
|
||||
"Return team ids for an organization.
|
||||
|
||||
Accepts either `cfg` and `organization-id` (fetches the org summary from
|
||||
Nitrate) or an already-resolved org summary map."
|
||||
([cfg organization-id]
|
||||
(get-org-team-ids (nitrate/call cfg :get-org-summary {:organization-id organization-id})))
|
||||
([org-summary]
|
||||
(->> (:teams org-summary)
|
||||
(map :id)
|
||||
(filter uuid?)
|
||||
(vec))))
|
||||
|
||||
(defn get-org-invitations
|
||||
"Fetch valid org-level and team-level invitations for an organization."
|
||||
[conn organization-id team-ids]
|
||||
(let [ids-array (db/create-array conn "uuid" team-ids)]
|
||||
(db/exec! conn [sql:get-org-invitations organization-id ids-array])))
|
||||
|
||||
(defn get-team-invitation-emails
|
||||
"Return distinct valid team invitation recipient emails."
|
||||
[conn team-id]
|
||||
(db/exec! conn [sql:get-team-invitation-emails team-id]))
|
||||
@ -6,10 +6,12 @@
|
||||
|
||||
(ns backend-tests.email-sending-test
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as emails]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
@ -23,3 +25,67 @@
|
||||
(t/is (contains? result :to))
|
||||
#_(t/is (contains? result :reply-to))
|
||||
(t/is (map? (:body result)))))
|
||||
|
||||
(def ^:private sso-notice-snippet
|
||||
"has set up single sign-on (SSO) in Penpot")
|
||||
|
||||
(defn- email-text-body
|
||||
[result]
|
||||
(get-in result [:body "text/plain"]))
|
||||
|
||||
(defn- invite-email-params
|
||||
[organization]
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:user-name "Invitee User"
|
||||
:token "test-token"
|
||||
:organization organization})
|
||||
|
||||
(t/deftest invite-to-org-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active true}))]
|
||||
(t/is (str/includes? (email-text-body result) sso-notice-snippet))
|
||||
(t/is (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet))))
|
||||
|
||||
(t/deftest invite-to-org-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-org
|
||||
(invite-email-params {:name "Acme Inc"
|
||||
:sso-active false}))]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
(t/deftest invite-to-team-includes-sso-notice-when-active
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"
|
||||
:organization {:name "Acme Inc"
|
||||
:sso-active true}})]
|
||||
(t/is (str/includes? (email-text-body result) sso-notice-snippet))
|
||||
(t/is (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet))))
|
||||
|
||||
(t/deftest invite-to-team-omits-sso-notice-when-inactive
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"
|
||||
:organization {:name "Acme Inc"
|
||||
:sso-active false}})]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
(t/deftest invite-to-team-omits-sso-notice-without-organization
|
||||
(let [result (emails/render emails/invite-to-team
|
||||
{:to "invitee@example.com"
|
||||
:public-uri (cf/get :public-uri)
|
||||
:invited-by "Owner User"
|
||||
:team "Design Team"
|
||||
:token "test-token"})]
|
||||
(t/is (not (str/includes? (email-text-body result) sso-notice-snippet)))
|
||||
(t/is (not (str/includes? (get-in result [:body "text/html"]) sso-notice-snippet)))))
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.email :as eml]
|
||||
[app.msgbus :as mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
@ -1198,3 +1199,81 @@
|
||||
(t/is (some? rel1)))
|
||||
(let [rel2 (th/db-get :team-profile-rel {:team-id (:id extra-team) :profile-id (:id user)})]
|
||||
(t/is (some? rel2)))))
|
||||
|
||||
(t/deftest notify-org-sso-change-sends-setup-sso-email-once-per-recipient
|
||||
(let [owner (th/create-profile* 1 {:is-active true :fullname "Owner"})
|
||||
member (th/create-profile* 2 {:is-active true
|
||||
:fullname "Member"
|
||||
:email "member@example.com"})
|
||||
invited (th/create-profile* 3 {:is-active true
|
||||
:fullname "Invited User"
|
||||
:email "invited@example.com"})
|
||||
org-id (uuid/random)
|
||||
org-name "Acme Inc"
|
||||
team (th/create-team* 1 {:profile-id (:id owner)})
|
||||
org-summary {:id org-id
|
||||
:name org-name
|
||||
:teams [{:id (:id team)}]}
|
||||
sent (atom [])
|
||||
params {::th/type :notify-org-sso-change
|
||||
:organization-id org-id
|
||||
:updated-props false
|
||||
:became-active true}]
|
||||
|
||||
;; Member also has a pending invitation: should still receive only one email.
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:org-id org-id
|
||||
:team-id nil
|
||||
:email-to (:email member)
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "24h")})
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to (:email invited)
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
;; Invite without an existing profile.
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "72h")})
|
||||
|
||||
(with-redefs [nitrate/call (fn [_cfg method _params]
|
||||
(case method
|
||||
:get-org-members [(:id owner) (:id member)]
|
||||
:get-org-summary org-summary
|
||||
nil))
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(management-command-with-nitrate! params))
|
||||
|
||||
(let [emails (->> @sent (map :to) set)]
|
||||
(t/is (= 4 (count @sent)))
|
||||
(t/is (= #{"member@example.com"
|
||||
(:email owner)
|
||||
"invited@example.com"
|
||||
"external@example.com"}
|
||||
emails))
|
||||
(doseq [email-params @sent]
|
||||
(t/is (= org-name (:organization-name email-params)))
|
||||
(t/is (= eml/organization-setup-sso (::eml/factory email-params)))))))
|
||||
|
||||
(t/deftest notify-org-sso-change-skips-email-when-not-active
|
||||
(let [sent (atom [])
|
||||
params {::th/type :notify-org-sso-change
|
||||
:organization-id (uuid/random)
|
||||
:updated-props false
|
||||
:became-active false}]
|
||||
(with-redefs [eml/send! (fn [params] (swap! sent conj params))]
|
||||
(management-command-with-nitrate! params))
|
||||
(t/is (empty? @sent))))
|
||||
|
||||
@ -6,12 +6,15 @@
|
||||
|
||||
(ns backend-tests.rpc-nitrate-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as-alias db]
|
||||
[app.email :as eml]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.nitrate]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]))
|
||||
@ -815,3 +818,118 @@
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (= :validation (th/ex-type (:error out))))
|
||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
||||
|
||||
(defn- add-team-to-org-nitrate-mock
|
||||
[{:keys [org-id org-summary org-perms owner-id team-id sso-active?]}]
|
||||
(fn [_cfg method params]
|
||||
(case method
|
||||
:get-org-membership (if (= (:profile-id params) owner-id)
|
||||
{:is-member true :organization-id org-id}
|
||||
{:is-member false :organization-id org-id})
|
||||
:get-org-members [owner-id]
|
||||
:get-team-org {:organization nil}
|
||||
:get-org-permissions org-perms
|
||||
:set-team-org {:id team-id}
|
||||
:get-org-sso {:active sso-active?}
|
||||
:get-org-summary (assoc org-summary :teams [{:id team-id}])
|
||||
:add-profile-to-org {:is-member true}
|
||||
nil)))
|
||||
|
||||
(t/deftest add-team-to-organization-sends-sso-emails-to-new-members-and-invitees
|
||||
(let [owner (th/create-profile* 301 {:is-active true
|
||||
:fullname "Owner"
|
||||
:email "owner301@example.com"})
|
||||
member (th/create-profile* 302 {:is-active true
|
||||
:fullname "Member"
|
||||
:email "member302@example.com"})
|
||||
team (th/create-team* 301 {:profile-id (:id owner)})
|
||||
_ (th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
org-id (uuid/random)
|
||||
org-name "SSO Org"
|
||||
org-summary {:id org-id
|
||||
:name org-name
|
||||
:owner-id (:id owner)
|
||||
:teams []}
|
||||
org-perms {:owner-id (:id owner)
|
||||
:permissions {:create-teams "any"
|
||||
:move-teams "always"
|
||||
:new-team-members "members"}}
|
||||
sent (atom [])]
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external301@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/call (add-team-to-org-nitrate-mock
|
||||
{:org-id org-id
|
||||
:org-summary org-summary
|
||||
:org-perms org-perms
|
||||
:owner-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:sso-active? true})
|
||||
teams/initialize-user-in-nitrate-org (fn [& _] nil)
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(let [out (th/command! {::th/type :add-team-to-organization
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:organization-id org-id})]
|
||||
(t/is (th/success? out))))
|
||||
|
||||
(let [emails (->> @sent (map :to) set)]
|
||||
(t/is (= 2 (count @sent)))
|
||||
(t/is (= #{"member302@example.com" "external301@example.com"} emails))
|
||||
(doseq [email-params @sent]
|
||||
(t/is (= org-name (:organization-name email-params)))
|
||||
(t/is (= eml/organization-setup-sso (::eml/factory email-params)))))))
|
||||
|
||||
(t/deftest add-team-to-organization-skips-sso-emails-when-sso-inactive
|
||||
(let [owner (th/create-profile* 303 {:is-active true :email "owner303@example.com"})
|
||||
member (th/create-profile* 304 {:is-active true :email "member304@example.com"})
|
||||
team (th/create-team* 303 {:profile-id (:id owner)})
|
||||
_ (th/create-team-role* {:team-id (:id team)
|
||||
:profile-id (:id member)
|
||||
:role :editor})
|
||||
org-id (uuid/random)
|
||||
org-summary {:id org-id
|
||||
:name "No SSO Org"
|
||||
:owner-id (:id owner)
|
||||
:teams []}
|
||||
org-perms {:owner-id (:id owner)
|
||||
:permissions {:create-teams "any"
|
||||
:move-teams "always"
|
||||
:new-team-members "members"}}
|
||||
sent (atom [])]
|
||||
|
||||
(th/db-insert! :team-invitation
|
||||
{:id (uuid/random)
|
||||
:team-id (:id team)
|
||||
:org-id nil
|
||||
:email-to "external303@example.com"
|
||||
:created-by (:id owner)
|
||||
:role "editor"
|
||||
:valid-until (ct/in-future "48h")})
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||
nitrate/call (add-team-to-org-nitrate-mock
|
||||
{:org-id org-id
|
||||
:org-summary org-summary
|
||||
:org-perms org-perms
|
||||
:owner-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:sso-active? false})
|
||||
teams/initialize-user-in-nitrate-org (fn [& _] nil)
|
||||
eml/send! (fn [params] (swap! sent conj params))]
|
||||
(let [out (th/command! {::th/type :add-team-to-organization
|
||||
::rpc/profile-id (:id owner)
|
||||
:team-id (:id team)
|
||||
:organization-id org-id})]
|
||||
(t/is (th/success? out))
|
||||
(t/is (empty? @sent))))))
|
||||
|
||||
@ -61,4 +61,5 @@
|
||||
[:name ::sm/text]
|
||||
[:initials [:maybe :string]]
|
||||
[:logo [:maybe ::sm/uri]]
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]])
|
||||
[:avatar-bg-url [:maybe ::sm/uri]]
|
||||
[:sso-active {:optional true} [:maybe :boolean]]])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user