mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
✨ Add invite-to-org to Nitrate API
This commit is contained in:
parent
707cc53ca4
commit
5c761125f3
259
backend/resources/app/email/invite-to-org/en.html
Normal file
259
backend/resources/app/email/invite-to-org/en.html
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
<!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/uxbox-title.png"
|
||||||
|
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{{ user-name|abbreviate:25 }},
|
||||||
|
</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;">
|
||||||
|
<b>{{invited-by|abbreviate:25}}</b> sent you an invitation to join the organization {{ org-name|abbreviate:25 }}:
|
||||||
|
</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;">
|
||||||
|
<span style="display:inline-block;vertical-align:middle;width:20px;height:20px;border-radius: 50%;background-image:url('{{org-logo}}');overflow:hidden;text-align:center;font-weight: bold;font-size: 9px;line-height: 20px;">
|
||||||
|
{{ org-initials }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
||||||
|
“{{ org-name|abbreviate:25 }}”
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" vertical-align="middle"
|
||||||
|
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="border-collapse:separate;line-height:100%;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" bgcolor="#6911d4" role="presentation"
|
||||||
|
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
||||||
|
valign="middle">
|
||||||
|
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
||||||
|
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
||||||
|
target="_blank"> ACCEPT INVITE </a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</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;">
|
||||||
|
Enjoy!</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>
|
||||||
1
backend/resources/app/email/invite-to-org/en.subj
Normal file
1
backend/resources/app/email/invite-to-org/en.subj
Normal file
@ -0,0 +1 @@
|
|||||||
|
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}”
|
||||||
10
backend/resources/app/email/invite-to-org/en.txt
Normal file
10
backend/resources/app/email/invite-to-org/en.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Hello!
|
||||||
|
|
||||||
|
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ org-name|abbreviate:25 }}”.
|
||||||
|
|
||||||
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||||
|
|
||||||
|
Enjoy!
|
||||||
|
The Penpot team.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
Hello!
|
Hello!
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
|
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
||||||
|
|
||||||
Accept invitation using this link:
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
|||||||
@ -412,6 +412,21 @@
|
|||||||
:id ::invite-to-team
|
:id ::invite-to-team
|
||||||
:schema schema:invite-to-team))
|
:schema schema:invite-to-team))
|
||||||
|
|
||||||
|
(def ^:private schema:invite-to-org
|
||||||
|
[:map
|
||||||
|
[:invited-by ::sm/text]
|
||||||
|
[:org-name ::sm/text]
|
||||||
|
[:org-initials ::sm/text]
|
||||||
|
[:org-logo ::sm/uri]
|
||||||
|
[:user-name [:maybe ::sm/text]]
|
||||||
|
[:token ::sm/text]])
|
||||||
|
|
||||||
|
(def invite-to-org
|
||||||
|
"Org member invitation email."
|
||||||
|
(template-factory
|
||||||
|
:id ::invite-to-org
|
||||||
|
:schema schema:invite-to-org))
|
||||||
|
|
||||||
(def ^:private schema:join-team
|
(def ^:private schema:join-team
|
||||||
[:map
|
[:map
|
||||||
[:invited-by ::sm/text]
|
[:invited-by ::sm/text]
|
||||||
|
|||||||
@ -466,7 +466,11 @@
|
|||||||
:fn mg0145/migrate}
|
:fn mg0145/migrate}
|
||||||
|
|
||||||
{:name "0146-mod-access-token-table"
|
{:name "0146-mod-access-token-table"
|
||||||
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}])
|
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}
|
||||||
|
|
||||||
|
{:name "0147-mod-team-invitation-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0147-mod-team-invitation-table.sql")}])
|
||||||
|
|
||||||
|
|
||||||
(defn apply-migrations!
|
(defn apply-migrations!
|
||||||
[pool name migrations]
|
[pool name migrations]
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE team_invitation
|
||||||
|
ADD COLUMN org_id uuid NULL;
|
||||||
|
|
||||||
|
ALTER TABLE team_invitation
|
||||||
|
ALTER COLUMN team_id DROP NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE team_invitation
|
||||||
|
ADD CONSTRAINT team_invitation_team_or_org_not_null
|
||||||
|
CHECK (team_id IS NOT NULL OR org_id IS NOT NULL);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX team_invitation_org_unique
|
||||||
|
ON team_invitation (org_id, email_to)
|
||||||
|
WHERE team_id IS NULL;
|
||||||
@ -539,35 +539,29 @@
|
|||||||
team (create-team cfg params)]
|
team (create-team cfg params)]
|
||||||
(select-keys team [:id])))
|
(select-keys team [:id])))
|
||||||
|
|
||||||
(defn- initialize-user-in-nitrate-org
|
(defn initialize-user-in-nitrate-org
|
||||||
"If needed, create a default team for the user on the organization,
|
"If needed, create a default team for the user on the organization,
|
||||||
and notify Nitrate that an user has been added to an org."
|
and notify Nitrate that an user has been added to an org."
|
||||||
[cfg profile-id team-id]
|
[cfg profile-id org-id]
|
||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection-map? cfg)
|
||||||
"expected cfg with valid connection")
|
"expected cfg with valid connection")
|
||||||
(let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})]
|
(when (contains? cf/flags :nitrate)
|
||||||
;; Only when the team belong to an organization and the user is not a member
|
(db/tx-run!
|
||||||
(when (and
|
cfg
|
||||||
(some? (:organization-id membership)) ;; the team do belong to an organization
|
(fn [{:keys [::db/conn] :as tx-cfg}]
|
||||||
(not (:is-member membership))) ;; the user is not a member of the org yet
|
(let [org-id org-id
|
||||||
|
default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id)
|
||||||
(db/tx-run!
|
default-team-id (:id default-team)
|
||||||
cfg
|
result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id
|
||||||
(fn [{:keys [::db/conn] :as tx-cfg}]
|
:team-id default-team-id
|
||||||
(let [org-id (:organization-id membership)
|
:org-id org-id})]
|
||||||
default-team (create-default-org-team (assoc tx-cfg ::db/conn conn) profile-id org-id)
|
(when (not (:is-member result))
|
||||||
default-team-id (:id default-team)
|
(ex/raise :type :internal
|
||||||
result (nitrate/call tx-cfg :add-profile-to-org {:profile-id profile-id
|
:code :failed-add-profile-org-nitrate
|
||||||
:team-id default-team-id
|
:context {:profile-id profile-id
|
||||||
:org-id org-id})]
|
:org-id org-id
|
||||||
(when (not (:is-member result))
|
:default-team-id default-team-id}))
|
||||||
(ex/raise :type :internal
|
default-team-id)))))
|
||||||
:code :failed-add-profile-org-nitrate
|
|
||||||
:context {:profile-id profile-id
|
|
||||||
:team-id team-id
|
|
||||||
:org-id org-id
|
|
||||||
:default-team-id default-team-id}))
|
|
||||||
nil))))))
|
|
||||||
|
|
||||||
(defn add-profile-to-team!
|
(defn add-profile-to-team!
|
||||||
([cfg params]
|
([cfg params]
|
||||||
@ -576,7 +570,12 @@
|
|||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection-map? cfg)
|
||||||
"expected cfg with valid connection")
|
"expected cfg with valid connection")
|
||||||
(when (contains? cf/flags :nitrate)
|
(when (contains? cf/flags :nitrate)
|
||||||
(initialize-user-in-nitrate-org cfg profile-id team-id))
|
(let [membership (nitrate/call cfg :get-org-membership-by-team {:profile-id profile-id :team-id team-id})]
|
||||||
|
;; Only when the team belong to an organization and the user is not a member
|
||||||
|
(when (and
|
||||||
|
(some? (:organization-id membership)) ;; the team do belong to an organization
|
||||||
|
(not (:is-member membership))) ;; the user is not a member of the org yet
|
||||||
|
(initialize-user-in-nitrate-org cfg profile-id (:organization-id membership)))))
|
||||||
(db/insert! conn :team-profile-rel params options)))
|
(db/insert! conn :team-profile-rel params options)))
|
||||||
|
|
||||||
(defn create-team
|
(defn create-team
|
||||||
|
|||||||
@ -36,20 +36,29 @@
|
|||||||
;; --- Mutation: Create Team Invitation
|
;; --- Mutation: Create Team Invitation
|
||||||
|
|
||||||
(def sql:upsert-team-invitation
|
(def sql:upsert-team-invitation
|
||||||
"insert into team_invitation(id, team_id, email_to, created_by, role, valid_until)
|
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
||||||
values (?, ?, ?, ?, ?, ?)
|
values (?, ?, null, ?, ?, ?, ?)
|
||||||
on conflict(team_id, email_to) do
|
on conflict(team_id, email_to) do
|
||||||
update set role = ?, valid_until = ?, updated_at = now()
|
update set role = ?, valid_until = ?, updated_at = now()
|
||||||
returning *")
|
returning *")
|
||||||
|
|
||||||
|
(def sql:upsert-org-invitation
|
||||||
|
"insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
|
||||||
|
values (?, null, ?, ?, ?, ?, ?)
|
||||||
|
on conflict(org_id, email_to) where team_id is null do
|
||||||
|
update set role = ?, valid_until = ?, updated_at = now()
|
||||||
|
returning *")
|
||||||
|
|
||||||
(defn- create-invitation-token
|
(defn- create-invitation-token
|
||||||
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
|
[cfg {:keys [profile-id valid-until org-id org-name team-id member-id member-email role]}]
|
||||||
(tokens/generate cfg
|
(tokens/generate cfg
|
||||||
{:iss :team-invitation
|
{:iss :team-invitation
|
||||||
:exp valid-until
|
:exp valid-until
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:role role
|
:role role
|
||||||
:team-id team-id
|
:team-id team-id
|
||||||
|
:org-id org-id
|
||||||
|
:org-name org-name
|
||||||
:member-email member-email
|
:member-email member-email
|
||||||
:member-id member-id}))
|
:member-id member-id}))
|
||||||
|
|
||||||
@ -75,20 +84,40 @@
|
|||||||
[:role types.team/schema:role]
|
[:role types.team/schema:role]
|
||||||
[:email ::sm/email]])
|
[:email ::sm/email]])
|
||||||
|
|
||||||
|
(def ^:private schema:create-org-invitation
|
||||||
|
[:map {:title "params:create-org-invitation"}
|
||||||
|
[::rpc/profile-id ::sm/uuid]
|
||||||
|
[:organization
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:logo ::sm/uri]]]
|
||||||
|
[:profile
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:fullname :string]]]
|
||||||
|
[:role types.team/schema:role]
|
||||||
|
[:email ::sm/email]])
|
||||||
|
|
||||||
(def ^:private check-create-invitation-params
|
(def ^:private check-create-invitation-params
|
||||||
(sm/check-fn schema:create-invitation))
|
(sm/check-fn schema:create-invitation))
|
||||||
|
|
||||||
|
(def ^:private check-create-org-invitation-params
|
||||||
|
(sm/check-fn schema:create-org-invitation))
|
||||||
|
|
||||||
(defn- allow-invitation-emails?
|
(defn- allow-invitation-emails?
|
||||||
[member]
|
[member]
|
||||||
(let [notifications (dm/get-in member [:props :notifications])]
|
(let [notifications (dm/get-in member [:props :notifications])]
|
||||||
(not= :none (:email-invites notifications))))
|
(not= :none (:email-invites notifications))))
|
||||||
|
|
||||||
(defn- create-invitation
|
(defn- create-invitation
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team profile role email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [team organization profile role email] :as params}]
|
||||||
|
|
||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection-map? cfg)
|
||||||
"expected cfg with valid connection")
|
"expected cfg with valid connection")
|
||||||
(assert (check-create-invitation-params params))
|
(if organization
|
||||||
|
(assert (check-create-org-invitation-params params))
|
||||||
|
(assert (check-create-invitation-params params)))
|
||||||
|
|
||||||
(let [email (profile/clean-email email)
|
(let [email (profile/clean-email email)
|
||||||
member (profile/get-profile-by-email conn email)]
|
member (profile/get-profile-by-email conn email)]
|
||||||
@ -105,8 +134,12 @@
|
|||||||
:profile-id (:id member)}
|
:profile-id (:id member)}
|
||||||
(get types.team/permissions-for-role role))]
|
(get types.team/permissions-for-role role))]
|
||||||
|
|
||||||
;; Insert the invited member to the team
|
(if organization
|
||||||
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
;; Insert the invited member to the org
|
||||||
|
(when (contains? cf/flags :nitrate)
|
||||||
|
(teams/initialize-user-in-nitrate-org cfg (:id member) (:id organization)))
|
||||||
|
;; Insert the invited member to the team
|
||||||
|
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))
|
||||||
|
|
||||||
;; If profile is not yet verified, mark it as verified because
|
;; If profile is not yet verified, mark it as verified because
|
||||||
;; accepting an invitation link serves as verification.
|
;; accepting an invitation link serves as verification.
|
||||||
@ -123,18 +156,30 @@
|
|||||||
(teams/check-email-spam conn email true)
|
(teams/check-email-spam conn email true)
|
||||||
|
|
||||||
(let [id (uuid/next)
|
(let [id (uuid/next)
|
||||||
expire (ct/in-future "168h") ;; 7 days
|
expire (if organization
|
||||||
invitation (db/exec-one! conn [sql:upsert-team-invitation id
|
(ct/in-future "876000h") ;; Organization invitations doesn't expire
|
||||||
(:id team) (str/lower email)
|
(ct/in-future "168h")) ;; 7 days
|
||||||
(:id profile)
|
invitation (db/exec-one! conn (if organization
|
||||||
(name role) expire
|
[sql:upsert-org-invitation id
|
||||||
(name role) expire])
|
(:id organization)
|
||||||
|
(str/lower email)
|
||||||
|
(:id profile)
|
||||||
|
(name role) expire
|
||||||
|
(name role) expire]
|
||||||
|
[sql:upsert-team-invitation id
|
||||||
|
(:id team)
|
||||||
|
(str/lower email)
|
||||||
|
(:id profile)
|
||||||
|
(name role) expire
|
||||||
|
(name role) expire]))
|
||||||
updated? (not= id (:id invitation))
|
updated? (not= id (:id invitation))
|
||||||
profile-id (:id profile)
|
profile-id (:id profile)
|
||||||
tprops {:profile-id profile-id
|
tprops {:profile-id profile-id
|
||||||
:invitation-id (:id invitation)
|
:invitation-id (:id invitation)
|
||||||
:valid-until expire
|
:valid-until expire
|
||||||
:team-id (:id team)
|
:team-id (:id team)
|
||||||
|
:org-id (:id organization)
|
||||||
|
:org-name (:name organization)
|
||||||
:member-email (:email-to invitation)
|
:member-email (:email-to invitation)
|
||||||
:member-id (:id member)
|
:member-id (:id member)
|
||||||
:role role}
|
:role role}
|
||||||
@ -146,30 +191,54 @@
|
|||||||
|
|
||||||
(let [props (-> (dissoc tprops :profile-id)
|
(let [props (-> (dissoc tprops :profile-id)
|
||||||
(audit/clean-props))
|
(audit/clean-props))
|
||||||
evname (if updated?
|
evname (cond
|
||||||
"update-team-invitation"
|
(and updated? organization) "update-org-invitation"
|
||||||
"create-team-invitation")
|
updated? "update-team-invitation"
|
||||||
|
organization "create-org-invitation"
|
||||||
|
:else "create-team-invitation")
|
||||||
event (-> (audit/event-from-rpc-params params)
|
event (-> (audit/event-from-rpc-params params)
|
||||||
(assoc ::audit/name evname)
|
(assoc ::audit/name evname)
|
||||||
(assoc ::audit/props props))]
|
(assoc ::audit/props props))]
|
||||||
(audit/submit! cfg event))
|
(audit/submit! cfg event))
|
||||||
|
|
||||||
(when (allow-invitation-emails? member)
|
(when (allow-invitation-emails? member)
|
||||||
(let [team (if (contains? cf/flags :nitrate)
|
(if organization
|
||||||
(nitrate/add-org-info-to-team cfg team {})
|
(when (contains? cf/flags :nitrate)
|
||||||
team)]
|
(eml/send! {::eml/conn conn
|
||||||
(eml/send! {::eml/conn conn
|
::eml/factory eml/invite-to-org
|
||||||
::eml/factory eml/invite-to-team
|
:public-uri (cf/get :public-uri)
|
||||||
:public-uri (cf/get :public-uri)
|
:to email
|
||||||
:to email
|
:invited-by (:fullname profile)
|
||||||
:invited-by (:fullname profile)
|
:user-name (:fullname member)
|
||||||
:team (:name team)
|
:org-name (:name organization)
|
||||||
:organization (:organization-name team)
|
:org-logo (:logo organization)
|
||||||
:token itoken
|
:org-initials (d/get-initials (:name organization))
|
||||||
:extra-data ptoken})))
|
:token itoken
|
||||||
|
:extra-data ptoken}))
|
||||||
|
(let [team (if (contains? cf/flags :nitrate)
|
||||||
|
(nitrate/add-org-info-to-team cfg team {})
|
||||||
|
team)]
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/invite-to-team
|
||||||
|
:public-uri (cf/get :public-uri)
|
||||||
|
:to email
|
||||||
|
:invited-by (:fullname profile)
|
||||||
|
:team (:name team)
|
||||||
|
:organization (:organization-name team)
|
||||||
|
:token itoken
|
||||||
|
:extra-data ptoken}))))
|
||||||
|
|
||||||
itoken)))))
|
itoken)))))
|
||||||
|
|
||||||
|
(defn create-org-invitation
|
||||||
|
[cfg {:keys [::rpc/profile-id id name logo] :as params}]
|
||||||
|
(let [profile (db/get-by-id cfg :profile profile-id)]
|
||||||
|
(create-invitation cfg
|
||||||
|
(assoc params
|
||||||
|
:organization {:id id :name name :logo logo}
|
||||||
|
:profile profile
|
||||||
|
:role :editor))))
|
||||||
|
|
||||||
(defn- add-member-to-team
|
(defn- add-member-to-team
|
||||||
[{:keys [::db/conn] :as cfg} profile team role member]
|
[{:keys [::db/conn] :as cfg} profile team role member]
|
||||||
(assert (db/connection-map? cfg)
|
(assert (db/connection-map? cfg)
|
||||||
|
|||||||
@ -87,52 +87,74 @@
|
|||||||
;; --- Team Invitation
|
;; --- Team Invitation
|
||||||
|
|
||||||
(defn- accept-invitation
|
(defn- accept-invitation
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
|
[{:keys [::db/conn] :as cfg}
|
||||||
|
{:keys [team-id org-id role member-email] :as claims} invitation member]
|
||||||
(let [;; Update the role if there is an invitation
|
(let [;; Update the role if there is an invitation
|
||||||
role (or (some-> invitation :role keyword) role)
|
role (or (some-> invitation :role keyword) role)
|
||||||
params (merge
|
id-member (:id member)]
|
||||||
{:team-id team-id
|
|
||||||
:profile-id (:id member)}
|
|
||||||
(get types.team/permissions-for-role role))]
|
|
||||||
|
|
||||||
;; Do not allow blocked users accept invitations.
|
;; Do not allow blocked users accept invitations.
|
||||||
(when (:is-blocked member)
|
(when (:is-blocked member)
|
||||||
(ex/raise :type :restriction
|
(ex/raise :type :restriction
|
||||||
:code :profile-blocked))
|
:code :profile-blocked))
|
||||||
|
|
||||||
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
(when team-id
|
||||||
::quotes/profile-id (:id member)
|
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
|
||||||
::quotes/team-id team-id})
|
::quotes/profile-id id-member
|
||||||
|
::quotes/team-id team-id}))
|
||||||
|
|
||||||
;; Insert the invited member to the team
|
(let [params (merge
|
||||||
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
{:team-id team-id
|
||||||
|
:profile-id id-member}
|
||||||
|
(get types.team/permissions-for-role role))
|
||||||
|
|
||||||
;; If profile is not yet verified, mark it as verified because
|
accepted-team-id (if org-id
|
||||||
;; accepting an invitation link serves as verification.
|
;; Insert the invited member to the org
|
||||||
(when-not (:is-active member)
|
(when (contains? cf/flags :nitrate)
|
||||||
(db/update! conn :profile
|
(teams/initialize-user-in-nitrate-org cfg id-member org-id))
|
||||||
{:is-active true}
|
;; Insert the invited member to the team
|
||||||
{:id (:id member)}))
|
(do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
|
||||||
|
team-id))]
|
||||||
|
|
||||||
;; Delete the invitation
|
(when-not accepted-team-id
|
||||||
(db/delete! conn :team-invitation
|
(ex/raise :type :internal
|
||||||
{:team-id team-id :email-to member-email})
|
:code :accept-invitation-failed
|
||||||
|
:hint "the accept invitation has failed"))
|
||||||
|
|
||||||
;; Delete any request
|
|
||||||
(db/delete! conn :team-access-request
|
|
||||||
{:team-id team-id :requester-id (:id member)})
|
|
||||||
|
|
||||||
(assoc member :is-active 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 id-member}))
|
||||||
|
|
||||||
|
;; Delete the invitation
|
||||||
|
(db/delete! conn :team-invitation
|
||||||
|
(cond-> {:email-to member-email}
|
||||||
|
team-id (assoc :team-id team-id)
|
||||||
|
org-id (assoc :org-id org-id)))
|
||||||
|
|
||||||
|
;; Delete any request (only applicable for team invitations)
|
||||||
|
(when team-id
|
||||||
|
(db/delete! conn :team-access-request
|
||||||
|
{:team-id team-id :requester-id id-member}))
|
||||||
|
|
||||||
|
accepted-team-id)))
|
||||||
|
|
||||||
(def schema:team-invitation-claims
|
(def schema:team-invitation-claims
|
||||||
[:map {:title "TeamInvitationClaims"}
|
[:and
|
||||||
[:iss :keyword]
|
[:map {:title "TeamInvitationClaims"}
|
||||||
[:exp ::ct/inst]
|
[:iss :keyword]
|
||||||
[:profile-id ::sm/uuid]
|
[:exp ::ct/inst]
|
||||||
[:role types.team/schema:role]
|
[:profile-id ::sm/uuid]
|
||||||
[:team-id ::sm/uuid]
|
[:role types.team/schema:role]
|
||||||
[:member-email ::sm/email]
|
[:team-id {:optional true} ::sm/uuid]
|
||||||
[:member-id {:optional true} ::sm/uuid]])
|
[:org-id {:optional true} ::sm/uuid]
|
||||||
|
[:member-email ::sm/email]
|
||||||
|
[:member-id {:optional true} ::sm/uuid]]
|
||||||
|
[:fn {:error/message "team-id or org-id must be present"}
|
||||||
|
(fn [m] (or (:team-id m) (:org-id m)))]])
|
||||||
|
|
||||||
(def valid-team-invitation-claims?
|
(def valid-team-invitation-claims?
|
||||||
(sm/lazy-validator schema:team-invitation-claims))
|
(sm/lazy-validator schema:team-invitation-claims))
|
||||||
@ -140,7 +162,7 @@
|
|||||||
(defmethod process-token :team-invitation
|
(defmethod process-token :team-invitation
|
||||||
[{:keys [::db/conn] :as cfg}
|
[{:keys [::db/conn] :as cfg}
|
||||||
{:keys [::rpc/profile-id token] :as params}
|
{:keys [::rpc/profile-id token] :as params}
|
||||||
{:keys [member-id team-id member-email] :as claims}]
|
{:keys [member-id team-id org-id member-email] :as claims}]
|
||||||
|
|
||||||
(when-not (valid-team-invitation-claims? claims)
|
(when-not (valid-team-invitation-claims? claims)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
@ -148,19 +170,27 @@
|
|||||||
:hint "invitation token contains unexpected data"))
|
:hint "invitation token contains unexpected data"))
|
||||||
|
|
||||||
(let [invitation (db/get* conn :team-invitation
|
(let [invitation (db/get* conn :team-invitation
|
||||||
{:team-id team-id :email-to member-email})
|
(cond-> {:email-to member-email}
|
||||||
|
team-id (assoc :team-id team-id)
|
||||||
|
org-id (assoc :org-id org-id)))
|
||||||
profile (db/get* conn :profile
|
profile (db/get* conn :profile
|
||||||
{:id profile-id}
|
{:id profile-id}
|
||||||
{:columns [:id :email]})
|
{:columns [:id :email]})
|
||||||
registration-disabled? (not (contains? cf/flags :registration))]
|
registration-disabled? (not (contains? cf/flags :registration))]
|
||||||
|
|
||||||
(when (nil? invitation)
|
(when (nil? invitation)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-token
|
:code :invalid-token
|
||||||
:hint "no invitation associated with the token"))
|
:hint "no invitation associated with the token"))
|
||||||
|
|
||||||
(if (some? profile)
|
(if profile
|
||||||
(if (or (= member-id profile-id)
|
(do
|
||||||
(= member-email (:email profile)))
|
(when-not (or (= member-id profile-id)
|
||||||
|
(= member-email (:email profile)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-token
|
||||||
|
:hint "logged-in user does not matches the invitation"))
|
||||||
|
|
||||||
|
|
||||||
;; if we have logged-in user and it matches the invitation we proceed
|
;; if we have logged-in user and it matches the invitation we proceed
|
||||||
;; with accepting the invitation and joining the current profile to the
|
;; with accepting the invitation and joining the current profile to the
|
||||||
@ -188,17 +218,16 @@
|
|||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:email (:email profile))))))
|
:email (:email profile))))))
|
||||||
|
|
||||||
(accept-invitation cfg claims invitation profile)
|
(let [accepted-team-id (accept-invitation cfg claims invitation profile)]
|
||||||
(assoc claims :state :created))
|
(cond-> (assoc claims :state :created)
|
||||||
|
;; when the invitation is to an org, instead of a team, add the
|
||||||
(ex/raise :type :validation
|
;; accepted-team-id as :org-team-id
|
||||||
:code :invalid-token
|
(:org-id claims)
|
||||||
:hint "logged-in user does not matches the invitation"))
|
(assoc :org-team-id accepted-team-id)))))
|
||||||
|
|
||||||
;; If we have not logged-in user, and invitation comes with member-id we
|
;; If we have not logged-in user, and invitation comes with member-id we
|
||||||
;; redirect user to login, if no memeber-id is present and in the invitation
|
;; redirect user to login, if no memeber-id is present and in the invitation
|
||||||
;; token and registration is enabled, we redirect user the the register page.
|
;; token and registration is enabled, we redirect user the the register page.
|
||||||
|
|
||||||
{:invitation-token token
|
{:invitation-token token
|
||||||
:iss :team-invitation
|
:iss :team-invitation
|
||||||
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
|
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
|
[app.rpc.commands.teams-invitations :as ti]
|
||||||
[app.rpc.doc :as doc]
|
[app.rpc.doc :as doc]
|
||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
@ -294,3 +295,18 @@ RETURNING id, name;")
|
|||||||
:id id))
|
:id id))
|
||||||
(profile-to-map profile)))
|
(profile-to-map profile)))
|
||||||
|
|
||||||
|
|
||||||
|
;; API: invite-to-org
|
||||||
|
|
||||||
|
(sv/defmethod ::invite-to-org
|
||||||
|
"Invite to organization"
|
||||||
|
{::doc/added "2.15"
|
||||||
|
::sm/params [:map
|
||||||
|
[:email ::sm/email]
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name ::sm/text]
|
||||||
|
[:logo ::sm/uri]]}
|
||||||
|
[cfg params]
|
||||||
|
(db/tx-run! cfg ti/create-org-invitation params)
|
||||||
|
nil)
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as-alias db]
|
[app.db :as-alias db]
|
||||||
|
[app.email :as email]
|
||||||
[app.msgbus :as mbus]
|
[app.msgbus :as mbus]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[backend-tests.helpers :as th]
|
[backend-tests.helpers :as th]
|
||||||
|
|||||||
@ -1181,6 +1181,16 @@
|
|||||||
str/trim)
|
str/trim)
|
||||||
""))
|
""))
|
||||||
|
|
||||||
|
(defn get-initials
|
||||||
|
"Returns up to two uppercase initials extracted from a string.
|
||||||
|
Non-letter prefixes in each token are ignored."
|
||||||
|
[name]
|
||||||
|
(->> (str/split (str/trim (or name "")) #"\s+")
|
||||||
|
(keep #(first (re-seq #"[a-zA-Z]" %)))
|
||||||
|
(take 2)
|
||||||
|
(map str/upper)
|
||||||
|
(apply str)))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Util protocols
|
;; Util protocols
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -28,6 +28,14 @@
|
|||||||
(t/is (not (d/in-range? 5 -1)))
|
(t/is (not (d/in-range? 5 -1)))
|
||||||
(t/is (not (d/in-range? 0 0))))
|
(t/is (not (d/in-range? 0 0))))
|
||||||
|
|
||||||
|
(t/deftest get-initials-test
|
||||||
|
(t/is (= "JD" (d/get-initials "John Doe")))
|
||||||
|
(t/is (= "A" (d/get-initials "acme")))
|
||||||
|
(t/is (= "AB" (d/get-initials "123 Alpha ## beta")))
|
||||||
|
(t/is (= "PD" (d/get-initials " penpot design tool ")))
|
||||||
|
(t/is (= "" (d/get-initials nil)))
|
||||||
|
(t/is (= "" (d/get-initials "!!! ???"))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Ordered Data Structures
|
;; Ordered Data Structures
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -41,19 +41,22 @@
|
|||||||
(st/emit! (da/login-from-token tdata)))
|
(st/emit! (da/login-from-token tdata)))
|
||||||
|
|
||||||
(defmethod handle-token :team-invitation
|
(defmethod handle-token :team-invitation
|
||||||
[tdata]
|
[{:keys [state team-id org-team-id org-name invitation-token] :as tdata}]
|
||||||
(case (:state tdata)
|
(case state
|
||||||
:created
|
:created
|
||||||
(let [team-id (:team-id tdata)]
|
(if org-team-id
|
||||||
(st/emit!
|
(st/emit!
|
||||||
(ntf/success (tr "auth.notifications.team-invitation-accepted"))
|
|
||||||
(du/refresh-profile)
|
(du/refresh-profile)
|
||||||
(dcm/go-to-dashboard-recent :team-id team-id)))
|
(dcm/go-to-dashboard-recent :team-id org-team-id)
|
||||||
|
(ntf/success (tr "auth.notifications.org-invitation-accepted" org-name)))
|
||||||
|
(st/emit!
|
||||||
|
(du/refresh-profile)
|
||||||
|
(dcm/go-to-dashboard-recent :team-id team-id)
|
||||||
|
(ntf/success (tr "auth.notifications.team-invitation-accepted"))))
|
||||||
|
|
||||||
:pending
|
:pending
|
||||||
(let [token (:invitation-token tdata)
|
(let [route-id (:redirect-to tdata :auth-register)]
|
||||||
route-id (:redirect-to tdata :auth-register)]
|
(st/emit! (rt/nav route-id {:invitation-token invitation-token})))))
|
||||||
(st/emit! (rt/nav route-id {:invitation-token token})))))
|
|
||||||
|
|
||||||
(defmethod handle-token :default
|
(defmethod handle-token :default
|
||||||
[_tdata]
|
[_tdata]
|
||||||
|
|||||||
@ -7,24 +7,16 @@
|
|||||||
(ns app.main.ui.components.org-avatar
|
(ns app.main.ui.components.org-avatar
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[cuerdas.core :as str]
|
[app.common.data :as d]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(defn- get-org-initials
|
|
||||||
[name]
|
|
||||||
(->> (str/split (str/trim (or name "")) #"\s+")
|
|
||||||
(keep #(first (re-seq #"[a-zA-Z]" %)))
|
|
||||||
(take 2)
|
|
||||||
(map str/upper)
|
|
||||||
(apply str)))
|
|
||||||
|
|
||||||
(mf/defc org-avatar*
|
(mf/defc org-avatar*
|
||||||
{::mf/props :obj}
|
{::mf/props :obj}
|
||||||
[{:keys [org size]}]
|
[{:keys [org size]}]
|
||||||
(let [name (:name org)
|
(let [name (:name org)
|
||||||
custom-photo (:organization-custom-photo org)
|
custom-photo (:organization-custom-photo org)
|
||||||
avatar-bg (:organization-avatar-bg-url org)
|
avatar-bg (:organization-avatar-bg-url org)
|
||||||
initials (get-org-initials name)]
|
initials (d/get-initials name)]
|
||||||
|
|
||||||
(if custom-photo
|
(if custom-photo
|
||||||
[:img {:src custom-photo
|
[:img {:src custom-photo
|
||||||
|
|||||||
@ -108,6 +108,9 @@ msgstr "Password recovery link sent to your inbox."
|
|||||||
msgid "auth.notifications.team-invitation-accepted"
|
msgid "auth.notifications.team-invitation-accepted"
|
||||||
msgstr "Joined the team successfully"
|
msgstr "Joined the team successfully"
|
||||||
|
|
||||||
|
msgid "auth.notifications.org-invitation-accepted"
|
||||||
|
msgstr "You're now part of %s"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174
|
#: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174
|
||||||
msgid "auth.password"
|
msgid "auth.password"
|
||||||
msgstr "Password"
|
msgstr "Password"
|
||||||
|
|||||||
@ -114,6 +114,9 @@ msgstr "Hemos enviado a tu buzón un enlace para recuperar tu contraseña."
|
|||||||
msgid "auth.notifications.team-invitation-accepted"
|
msgid "auth.notifications.team-invitation-accepted"
|
||||||
msgstr "Te uniste al equipo"
|
msgstr "Te uniste al equipo"
|
||||||
|
|
||||||
|
msgid "auth.notifications.org-invitation-accepted"
|
||||||
|
msgstr "Te uniste a la organización %s"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174
|
#: src/app/main/ui/auth/login.cljs:188, src/app/main/ui/auth/register.cljs:174
|
||||||
msgid "auth.password"
|
msgid "auth.password"
|
||||||
msgstr "Contraseña"
|
msgstr "Contraseña"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user