feat(server): new base email template + implementation for invites emails (#903)
Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
This commit is contained in:
committed by
GitHub
parent
8585347a6f
commit
0427f5cfd1
@@ -46,3 +46,7 @@ events.json
|
||||
|
||||
# VSCode log files
|
||||
packages/server/.vscode/*.log
|
||||
|
||||
# ST workspace files
|
||||
./speckle.sublime-project
|
||||
./speckle.sublime-workspace
|
||||
@@ -0,0 +1,650 @@
|
||||
<!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]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</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>
|
||||
<style type="text/css"></style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing: normal">
|
||||
<div style="">
|
||||
<!-- Header -->
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="linear-gradient(90deg, rgba(0,143,233,1) 0%, rgba(0,76,235,1) 100%)" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
background-color: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
margin: 0px auto;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
background-color: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
"
|
||||
>
|
||||
<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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
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: 100px">
|
||||
<img
|
||||
alt="Speckle"
|
||||
height="auto"
|
||||
src="https://i.imgur.com/KAvbGEj.png"
|
||||
style="
|
||||
border: 0;
|
||||
display: block;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
"
|
||||
width="100"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Body - Start -->
|
||||
<% if (params.html?.bodyStart?.length) { %>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" 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: 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%"
|
||||
>
|
||||
<tbody>
|
||||
<!-- Some example text -->
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
<%- params.html.bodyStart -%>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
<% } %>
|
||||
|
||||
<!-- CTA -->
|
||||
<% if (params.cta?.url && params.cta?.title) { %>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" 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%"
|
||||
>
|
||||
<tbody>
|
||||
<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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
bgcolor="linear-gradient(90deg, rgba(0,143,233,1) 0%, rgba(0,76,235,1) 100%)"
|
||||
role="presentation"
|
||||
style="
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: auto;
|
||||
mso-padding-alt: 25px 55px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
"
|
||||
valign="middle"
|
||||
>
|
||||
<a
|
||||
href="<%= params.cta.url %>"
|
||||
title="<%= params.cta.altTitle || params.cta.title %>"
|
||||
rel="notrack"
|
||||
style="
|
||||
display: inline-block;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 143, 233, 1) 0%,
|
||||
rgba(0, 76, 235, 1) 100%
|
||||
);
|
||||
color: white;
|
||||
font-family: Helvetica;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
padding: 25px 55px;
|
||||
mso-padding-alt: 0px;
|
||||
border-radius: 8px;
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
<%- params.cta.title -%>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
<% } %>
|
||||
|
||||
<!-- Body - End -->
|
||||
<% if (params.html?.bodyEnd?.length) { %>
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" 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: 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%"
|
||||
>
|
||||
<tbody>
|
||||
<!-- Some example finishing text -->
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
<%- params.html.bodyEnd -%>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
<% } %>
|
||||
|
||||
<!-- Footer -->
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" 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="
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0;
|
||||
padding-top: 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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
"
|
||||
>
|
||||
Sent from <%= params.server.name || 'Speckle Server' %> at
|
||||
<a
|
||||
href="<%= params.server.url %>"
|
||||
title="<%= params.server?.name || 'Speckle Server' %>"
|
||||
>
|
||||
<%= params.server.url %>
|
||||
</a>
|
||||
, deployed and managed by <%= params.server.company %>. Your
|
||||
admin contact is <%= params.server.contact %>.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" 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: 20px 0;
|
||||
padding-top: 20px;
|
||||
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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: #e0e0e0;
|
||||
"
|
||||
>
|
||||
Brought to you by
|
||||
<a href="https://speckle.systems" target="_blank">
|
||||
Speckle
|
||||
</a>
|
||||
, the Open Source Data Platform for 3D Data
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<%_ if (params.text?.bodyStart?.length) print("\n" + params.text.bodyStart + "\n"); _%>
|
||||
<%_ if (params.cta) print("\n" + params.cta.title + ': ' + params.cta.url + "\n"); _%>
|
||||
<%_ if (params.text?.bodyEnd?.length) print("\n" + params.text.bodyEnd + "\n"); _%>
|
||||
|
||||
------------------------------------------------------
|
||||
Sent from <%= params.server.name || 'Speckle Server' %> at <%= params.server.url %>, deployed and managed by <%= params.server.company %>. Your admin contact is <%= params.server.contact %>.
|
||||
Vendored
+6
-6
@@ -3,17 +3,17 @@
|
||||
* Bootstrap module that should be imported at the very top of each entry point module
|
||||
*/
|
||||
|
||||
// Conditionally change appRoot and repoRoot according to whether we're running from /dist/ or not (ts-node)
|
||||
// Conditionally change appRoot and packageRoot according to whether we're running from /dist/ or not (ts-node)
|
||||
const path = require('path')
|
||||
const isTsNode = !!process[Symbol.for('ts-node.register.instance')]
|
||||
const appRoot = __dirname
|
||||
const repoRoot = isTsNode ? appRoot : path.resolve(__dirname, '../')
|
||||
const packageRoot = isTsNode ? appRoot : path.resolve(__dirname, '../')
|
||||
|
||||
// Initializing module aliases for absolute import paths
|
||||
const moduleAlias = require('module-alias')
|
||||
moduleAlias.addAliases({
|
||||
'@': appRoot,
|
||||
'#': repoRoot
|
||||
'#': packageRoot
|
||||
})
|
||||
|
||||
// Initializing env vars
|
||||
@@ -32,7 +32,7 @@ if (isApolloMonitoringEnabled() && !getApolloServerVersion()) {
|
||||
// If running in test env, load .env.test first
|
||||
// (appRoot necessary, cause env files aren't loaded through require() calls)
|
||||
if (isTestEnv()) {
|
||||
const { error } = dotenv.config({ path: `${repoRoot}/.env.test` })
|
||||
const { error } = dotenv.config({ path: `${packageRoot}/.env.test` })
|
||||
if (error) {
|
||||
const e = new Error(
|
||||
'Attempting to run tests without an .env.test file properly set up! Check readme!'
|
||||
@@ -42,9 +42,9 @@ if (isTestEnv()) {
|
||||
}
|
||||
}
|
||||
|
||||
dotenv.config({ path: `${repoRoot}/.env` })
|
||||
dotenv.config({ path: `${packageRoot}/.env` })
|
||||
|
||||
module.exports = {
|
||||
appRoot,
|
||||
repoRoot
|
||||
packageRoot
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
|
||||
const { repoRoot } = require('./bootstrap')
|
||||
const { packageRoot } = require('./bootstrap')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { isTestEnv } = require('@/modules/shared/helpers/envHelper')
|
||||
@@ -26,7 +26,7 @@ function walk(dir) {
|
||||
// The only exception is when running tests in the test DB, cause the stakes are way lower there and we always
|
||||
// run them through ts-node anyway, so it doesn't make sense forcing the app to be built
|
||||
const migrationModulesDir = path.resolve(
|
||||
repoRoot,
|
||||
packageRoot,
|
||||
isTestEnv() ? './modules' : './dist/modules'
|
||||
)
|
||||
if (!fs.existsSync(migrationModulesDir)) {
|
||||
|
||||
@@ -6,7 +6,7 @@ const commentsServiceMock = mockRequireModule(
|
||||
)
|
||||
|
||||
const path = require('path')
|
||||
const { repoRoot } = require('@/bootstrap')
|
||||
const { packageRoot } = require('@/bootstrap')
|
||||
const expect = require('chai').expect
|
||||
const crs = require('crypto-random-string')
|
||||
const { beforeEachContext, truncateTables } = require('@/test/hooks')
|
||||
@@ -1018,7 +1018,7 @@ describe('Comments @comments', () => {
|
||||
// Upload a small blob
|
||||
blob1 = await uploadBlob(
|
||||
app,
|
||||
path.resolve(repoRoot, './test/assets/testimage1.jpg'),
|
||||
path.resolve(packageRoot, './test/assets/testimage1.jpg'),
|
||||
stream.id,
|
||||
{
|
||||
authToken: userToken
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
const knex = require('@/db/knex')
|
||||
|
||||
/**
|
||||
* Single source of truth for DB schema in the codebase
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* ServerInvites:
|
||||
* - Get rid of the 'used' field, it's not used anymore
|
||||
*
|
||||
* TODO: Redo this when we have TS support with nice typing, ability to get columns with/without aliases
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
Streams: {
|
||||
name: 'streams',
|
||||
knex: () => knex('streams'),
|
||||
col: {
|
||||
id: 'streams.id',
|
||||
name: 'streams.name',
|
||||
description: 'streams.description',
|
||||
isPublic: 'streams.isPublic',
|
||||
clonedFrom: 'streams.clonedFrom',
|
||||
createdAt: 'streams.createdAt',
|
||||
updatedAt: 'streams.updatedAt'
|
||||
}
|
||||
},
|
||||
StreamAcl: {
|
||||
name: 'stream_acl',
|
||||
knex: () => knex('stream_acl'),
|
||||
col: {
|
||||
userId: 'stream_acl.userId',
|
||||
resourceId: 'stream_acl.resourceId',
|
||||
role: 'stream_acl.role'
|
||||
}
|
||||
},
|
||||
StreamFavorites: {
|
||||
name: 'stream_favorites',
|
||||
knex: () => knex('stream_favorites'),
|
||||
col: {
|
||||
streamId: 'stream_favorites.streamId',
|
||||
userId: 'stream_favorites.userId',
|
||||
createdAt: 'stream_favorites.createdAt',
|
||||
cursor: 'stream_favorites.cursor'
|
||||
}
|
||||
},
|
||||
Users: {
|
||||
name: 'users',
|
||||
knex: () => knex('users'),
|
||||
col: {
|
||||
id: 'users.id',
|
||||
suuid: 'users.suuid',
|
||||
createdAt: 'users.createdAt',
|
||||
name: 'users.name',
|
||||
bio: 'users.bio',
|
||||
company: 'users.company',
|
||||
email: 'users.email',
|
||||
verified: 'users.verified',
|
||||
avatar: 'users.avatar',
|
||||
profiles: 'users.profiles',
|
||||
passwordDigest: 'users.passwordDigest',
|
||||
ip: 'users.ip'
|
||||
}
|
||||
},
|
||||
ServerAcl: {
|
||||
name: 'server_acl',
|
||||
knex: () => knex('server_acl'),
|
||||
col: {
|
||||
userId: 'server_acl.userId',
|
||||
role: 'server_acl.role'
|
||||
}
|
||||
},
|
||||
Comments: {
|
||||
name: 'comments',
|
||||
knex: () => knex('comments'),
|
||||
col: {
|
||||
id: 'comments.id',
|
||||
streamId: 'comments.streamId',
|
||||
authorId: 'comments.authorId',
|
||||
createdAt: 'comments.createdAt',
|
||||
updatedAt: 'comments.updatedAt',
|
||||
text: 'comments.text',
|
||||
screenshot: 'comments.screenshot',
|
||||
data: 'comments.data',
|
||||
archived: 'comments.archived',
|
||||
parentComment: 'comments.parentComment'
|
||||
}
|
||||
},
|
||||
ServerInvites: {
|
||||
name: 'server_invites',
|
||||
knex: () => knex('server_invites'),
|
||||
col: {
|
||||
id: 'server_invites.id',
|
||||
target: 'server_invites.target',
|
||||
inviterId: 'server_invites.inviterId',
|
||||
createdAt: 'server_invites.createdAt',
|
||||
used: 'server_invites.used',
|
||||
message: 'server_invites.message',
|
||||
resourceTarget: 'server_invites.resourceTarget',
|
||||
resourceId: 'server_invites.resourceId',
|
||||
role: 'server_invites.role',
|
||||
token: 'server_invites.token'
|
||||
}
|
||||
},
|
||||
knex
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import knex from '@/db/knex'
|
||||
import { Knex } from 'knex'
|
||||
import { reduce } from 'lodash'
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* ServerInvites:
|
||||
* - Get rid of the 'used' field, it's not used anymore
|
||||
*/
|
||||
|
||||
type SchemaConfig<T extends string, C extends string> = InnerSchemaConfig<T, C> & {
|
||||
/**
|
||||
* Return schema helper with custom configuration options
|
||||
*/
|
||||
with: (params?: SchemaConfigParams) => InnerSchemaConfig<T, C>
|
||||
}
|
||||
|
||||
type InnerSchemaConfig<T extends string, C extends string> = {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
name: T
|
||||
/**
|
||||
* Get `knex(tableName)` QueryBuilder instance
|
||||
*/
|
||||
knex: () => Knex.QueryBuilder
|
||||
/**
|
||||
* Get names of table columns. The names can be prefixed with the table name or not, depending
|
||||
* on whether `withoutTablePrefix` was set when accessing the helper.
|
||||
*/
|
||||
col: {
|
||||
[colName in C]: string
|
||||
}
|
||||
}
|
||||
|
||||
type SchemaConfigParams = {
|
||||
/**
|
||||
* Configure `col` properties to not have the table name prefixed. For the most part you want the prefix,
|
||||
* cause this helps in queries with JOINS (when multiple tables have a col with the same name), but you don't
|
||||
* want the prefix when triggering UPDATE queries, because the `SET <name> = <value>` syntax doesn't support
|
||||
* column names with table prefixes.
|
||||
*/
|
||||
withoutTablePrefix?: boolean
|
||||
}
|
||||
|
||||
function buildTableHelper<T extends string, C extends string>(
|
||||
tableName: T,
|
||||
columns: C[]
|
||||
): SchemaConfig<T, C> {
|
||||
function buildInnerSchemaConfig(
|
||||
params: SchemaConfigParams = {}
|
||||
): InnerSchemaConfig<T, C> {
|
||||
return {
|
||||
name: tableName,
|
||||
knex: () => knex(tableName),
|
||||
col: reduce(
|
||||
columns,
|
||||
(prev, curr) => {
|
||||
prev[curr] = params.withoutTablePrefix ? curr : `${tableName}.${curr}`
|
||||
return prev
|
||||
},
|
||||
{} as Record<C, string>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...buildInnerSchemaConfig(),
|
||||
with: buildInnerSchemaConfig
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TABLE RECORD TYPES
|
||||
*/
|
||||
|
||||
export type ServerInviteRecord = {
|
||||
id: string
|
||||
target: string
|
||||
inviterId: string
|
||||
createdAt?: Date
|
||||
used?: boolean
|
||||
message?: string
|
||||
resourceTarget?: string
|
||||
resourceId?: string
|
||||
role?: string
|
||||
token: string
|
||||
}
|
||||
|
||||
/*
|
||||
* TABLE HELPERS
|
||||
* The generated helpers are used like this:
|
||||
*
|
||||
* Streams.name - TableName
|
||||
* Streams.col.id - Get column names
|
||||
* Streams.knex() - Get knex() instance for this specific table
|
||||
*
|
||||
* Streams.with({...}) - configure helper, e.g. disable table name being prefixed to col names:
|
||||
* Streams.with({withoutTablePrefix: true}).col.id
|
||||
*/
|
||||
|
||||
export const Streams = buildTableHelper('streams', [
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'isPublic',
|
||||
'clonedFrom',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
])
|
||||
|
||||
export const StreamAcl = buildTableHelper('stream_acl', [
|
||||
'userId',
|
||||
'resourceId',
|
||||
'role'
|
||||
])
|
||||
|
||||
export const StreamFavorites = buildTableHelper('stream_favorites', [
|
||||
'streamId',
|
||||
'userId',
|
||||
'createdAt',
|
||||
'cursor'
|
||||
])
|
||||
|
||||
export const Users = buildTableHelper('users', [
|
||||
'id',
|
||||
'suuid',
|
||||
'createdAt',
|
||||
'name',
|
||||
'bio',
|
||||
'company',
|
||||
'email',
|
||||
'verified',
|
||||
'avatar',
|
||||
'profiles',
|
||||
'passwordDigest',
|
||||
'ip'
|
||||
])
|
||||
|
||||
export const ServerAcl = buildTableHelper('server_acl', ['userId', 'role'])
|
||||
|
||||
export const Comments = buildTableHelper('comments', [
|
||||
'id',
|
||||
'streamId',
|
||||
'authorId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'text',
|
||||
'screenshot',
|
||||
'data',
|
||||
'archived',
|
||||
'parentComment'
|
||||
])
|
||||
|
||||
export const ServerInvites = buildTableHelper('server_invites', [
|
||||
'id',
|
||||
'target',
|
||||
'inviterId',
|
||||
'createdAt',
|
||||
'used',
|
||||
'message',
|
||||
'resourceTarget',
|
||||
'resourceId',
|
||||
'role',
|
||||
'token'
|
||||
])
|
||||
|
||||
export { knex }
|
||||
@@ -13,7 +13,7 @@ const { createPersonalAccessToken } = require('../services/tokens')
|
||||
const { beforeEachContext } = require(`@/test/hooks`)
|
||||
|
||||
const { sleep, noErrors } = require('@/test/helpers')
|
||||
const { repoRoot } = require('@/bootstrap')
|
||||
const { packageRoot } = require('@/bootstrap')
|
||||
const {
|
||||
addOrUpdateStreamCollaborator
|
||||
} = require('@/modules/core/services/streams/streamAccessService')
|
||||
@@ -70,7 +70,7 @@ describe('GraphQL API Subscriptions @gql-subscriptions', () => {
|
||||
serverProcess = childProcess.spawn(
|
||||
/^win/.test(process.platform) ? 'npm.cmd' : 'npm',
|
||||
['run', 'dev:server:test'],
|
||||
{ cwd: repoRoot.path }
|
||||
{ cwd: packageRoot }
|
||||
)
|
||||
|
||||
const reg = /running at 0.0.0.0:([0-9]*)/
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ServerInvites, Streams, Users } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
ServerInviteRecord,
|
||||
ServerInvites,
|
||||
Streams,
|
||||
Users
|
||||
} from '@/modules/core/dbSchema'
|
||||
import { truncateTables } from '@/test/hooks'
|
||||
import { createUser } from '@/modules/core/services/users'
|
||||
import { createStream } from '@/modules/core/services/streams'
|
||||
@@ -25,11 +30,13 @@ async function getOrderedInviteIds() {
|
||||
await ServerInvites.knex()
|
||||
.select(ServerInvites.col.id)
|
||||
.where(ServerInvites.col.target, 'NOT ILIKE', `@%`)
|
||||
).map((o) => o.id)
|
||||
).map((o: Pick<ServerInviteRecord, 'id'>) => o.id)
|
||||
}
|
||||
|
||||
async function getOrderedUserIds() {
|
||||
return (await Users.knex().select(Users.col.id)).map((o) => o.id)
|
||||
return (await Users.knex().select(Users.col.id)).map(
|
||||
(o: Pick<ServerInviteRecord, 'id'>) => o.id
|
||||
)
|
||||
}
|
||||
|
||||
describe('[Admin users list]', () => {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
'use strict'
|
||||
const debug = require('debug')('speckle')
|
||||
|
||||
const nodemailer = require('nodemailer')
|
||||
const modulesDebug = debug.extend('modules')
|
||||
const errorDebug = debug.extend('errors')
|
||||
|
||||
let transporter
|
||||
|
||||
const createJsonEchoTransporter = () =>
|
||||
nodemailer.createTransport({
|
||||
jsonTransport: true
|
||||
})
|
||||
|
||||
const initSmtpTransporter = async () => {
|
||||
try {
|
||||
const smtpTransporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT || 587,
|
||||
secure: process.env.EMAIL_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
})
|
||||
await smtpTransporter.verify()
|
||||
return smtpTransporter
|
||||
} catch {
|
||||
errorDebug('📧 Email provider is misconfigured, check config variables.')
|
||||
}
|
||||
}
|
||||
|
||||
const initTransporter = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return createJsonEchoTransporter()
|
||||
if (process.env.EMAIL === 'true') return await initSmtpTransporter()
|
||||
|
||||
modulesDebug(
|
||||
'📧 Email provider is not configured. Server functionality will be limited.'
|
||||
)
|
||||
}
|
||||
|
||||
exports.init = async (app) => {
|
||||
modulesDebug('📧 Init emails module')
|
||||
transporter = await initTransporter()
|
||||
require('./rest')(app)
|
||||
}
|
||||
|
||||
exports.finalize = async () => {
|
||||
// Nothing to do here.
|
||||
}
|
||||
|
||||
exports.sendEmail = async ({ from, to, subject, text, html }) => {
|
||||
// note, the transporter is only initialized with the app init step
|
||||
if (!transporter) {
|
||||
errorDebug('No email transport present. Cannot send emails.')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const emailFrom = process.env.EMAIL_FROM || 'no-reply@speckle.systems'
|
||||
return await transporter.sendMail({
|
||||
from: from || `"Speckle" <${emailFrom}>`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
})
|
||||
} catch (error) {
|
||||
errorDebug(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* istanbul ignore file */
|
||||
import * as SendingService from '@/modules/emails/services/sending'
|
||||
import { initializeTransporter } from '@/modules/emails/utils/transporter'
|
||||
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import dbg from 'debug'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
const debug = dbg('speckle')
|
||||
const modulesDebug = debug.extend('modules')
|
||||
|
||||
const emailsModule: SpeckleModule = {
|
||||
init: async (app) => {
|
||||
modulesDebug('📧 Init emails module')
|
||||
|
||||
// init transporter
|
||||
await initializeTransporter()
|
||||
|
||||
// init rest api
|
||||
;(await import('./rest')).default(app)
|
||||
},
|
||||
|
||||
finalize: noop
|
||||
}
|
||||
|
||||
async function sendEmail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
}: {
|
||||
from?: string
|
||||
to: string
|
||||
subject: string
|
||||
text: string
|
||||
html: string
|
||||
}) {
|
||||
return SendingService.sendEmail({ from, to, subject, text, html })
|
||||
}
|
||||
|
||||
export = {
|
||||
...emailsModule,
|
||||
sendEmail
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getTransporter } from '@/modules/emails/utils/transporter'
|
||||
import dbg from 'debug'
|
||||
|
||||
const debug = dbg('speckle')
|
||||
const errorDebug = debug.extend('errors')
|
||||
|
||||
/**
|
||||
* Send out an e-mail
|
||||
*/
|
||||
export async function sendEmail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
}: {
|
||||
from?: string
|
||||
to: string
|
||||
subject: string
|
||||
text: string
|
||||
html: string
|
||||
}): Promise<boolean> {
|
||||
const transporter = getTransporter()
|
||||
if (!transporter) {
|
||||
errorDebug('No email transport present. Cannot send emails.')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const emailFrom = process.env.EMAIL_FROM || 'no-reply@speckle.systems'
|
||||
return await transporter.sendMail({
|
||||
from: from || `"Speckle" <${emailFrom}>`,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html
|
||||
})
|
||||
} catch (error) {
|
||||
errorDebug(error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { packageRoot } from '@/bootstrap'
|
||||
import path from 'path'
|
||||
import ejs from 'ejs'
|
||||
|
||||
type MultiTypeEmailBody = {
|
||||
text: string
|
||||
html: string
|
||||
}
|
||||
|
||||
export type BasicEmailTemplateParams = {
|
||||
html: { bodyStart?: string; bodyEnd?: string }
|
||||
text: { bodyStart?: string; bodyEnd?: string }
|
||||
cta?: {
|
||||
url: string
|
||||
title: string
|
||||
altTitle?: string
|
||||
}
|
||||
server: {
|
||||
name: string
|
||||
url: string
|
||||
company: string
|
||||
contact: string
|
||||
}
|
||||
}
|
||||
|
||||
function getPathToTemplatesDir(): string {
|
||||
return path.resolve(packageRoot, './assets/emails/templates/')
|
||||
}
|
||||
|
||||
function buildTemplatePath(name: string, ext: string): string {
|
||||
return path.resolve(getPathToTemplatesDir(), `./${name}/${name}.${ext}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an e-mail body using the 'basic' template
|
||||
*/
|
||||
export async function buildBasicTemplateEmail(
|
||||
params: BasicEmailTemplateParams
|
||||
): Promise<MultiTypeEmailBody> {
|
||||
const textPath = buildTemplatePath('basic', 'txt')
|
||||
const htmlPath = buildTemplatePath('basic', 'html')
|
||||
|
||||
const [text, html] = await Promise.all([
|
||||
ejs.renderFile(textPath, { params }, { cache: true, outputFunctionName: 'print' }),
|
||||
ejs.renderFile(htmlPath, { params }, { cache: true, outputFunctionName: 'print' })
|
||||
])
|
||||
|
||||
return {
|
||||
text,
|
||||
html
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
const debug = require('debug')('speckle')
|
||||
|
||||
const nodemailer = require('nodemailer')
|
||||
const modulesDebug = debug.extend('modules')
|
||||
const errorDebug = debug.extend('errors')
|
||||
|
||||
/** @type {import('nodemailer').Transporter | undefined} */
|
||||
let transporter = undefined
|
||||
|
||||
const createJsonEchoTransporter = () =>
|
||||
nodemailer.createTransport({
|
||||
jsonTransport: true
|
||||
})
|
||||
|
||||
const initSmtpTransporter = async () => {
|
||||
try {
|
||||
const smtpTransporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT || 587,
|
||||
secure: process.env.EMAIL_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
})
|
||||
await smtpTransporter.verify()
|
||||
return smtpTransporter
|
||||
} catch {
|
||||
errorDebug('📧 Email provider is misconfigured, check config variables.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('nodemailer').Transporter | undefined}
|
||||
*/
|
||||
async function initializeTransporter() {
|
||||
let newTransporter = undefined
|
||||
|
||||
if (process.env.NODE_ENV === 'test') newTransporter = createJsonEchoTransporter()
|
||||
if (process.env.EMAIL === 'true') newTransporter = await initSmtpTransporter()
|
||||
|
||||
if (!newTransporter) {
|
||||
modulesDebug(
|
||||
'📧 Email provider is not configured. Server functionality will be limited.'
|
||||
)
|
||||
}
|
||||
|
||||
transporter = newTransporter
|
||||
return newTransporter
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {import('nodemailer').Transporter | undefined}
|
||||
*/
|
||||
function getTransporter() {
|
||||
return transporter
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeTransporter,
|
||||
getTransporter
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { appRoot, repoRoot } = require('@/bootstrap')
|
||||
const { appRoot, packageRoot } = require('@/bootstrap')
|
||||
const { values, merge, camelCase } = require('lodash')
|
||||
const baseTypeDefs = require('@/modules/core/graph/schema/baseTypeDefs')
|
||||
const { scalarResolvers } = require('./core/graph/scalars')
|
||||
@@ -61,9 +61,9 @@ exports.graph = () => {
|
||||
let schemaDirectives = {}
|
||||
|
||||
// load typedefs from /assets
|
||||
const assetModuleDirs = fs.readdirSync(`${repoRoot}/assets`)
|
||||
const assetModuleDirs = fs.readdirSync(`${packageRoot}/assets`)
|
||||
assetModuleDirs.forEach((dir) => {
|
||||
const typeDefDirPath = path.join(`${repoRoot}/assets`, dir, 'typedefs')
|
||||
const typeDefDirPath = path.join(`${packageRoot}/assets`, dir, 'typedefs')
|
||||
if (fs.existsSync(typeDefDirPath)) {
|
||||
const moduleSchemas = fs.readdirSync(typeDefDirPath)
|
||||
moduleSchemas.forEach((schema) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ const { getStream } = require('@/modules/core/repositories/streams')
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* Resolve resource from invite
|
||||
* @param {import('@/modules/serverinvites/helpers/inviteHelper').InviteResourceData} invite
|
||||
* @returns {Promise<Object>}
|
||||
@@ -126,9 +127,10 @@ async function updateAllInviteTargets(oldTargets, newTarget) {
|
||||
if (!oldTargets.length) return
|
||||
|
||||
// PostgreSQL doesn't support aliases in update calls for some reason...
|
||||
const ServerInvitesCols = ServerInvites.with({ withoutTablePrefix: true }).col
|
||||
await ServerInvites.knex()
|
||||
.whereIn(ServerInvites.col.target, oldTargets)
|
||||
.update('target', newTarget.toLowerCase())
|
||||
.whereIn(ServerInvitesCols.target, oldTargets)
|
||||
.update(ServerInvitesCols.target, newTarget.toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,9 @@ const { getUsers, getUser } = require('@/modules/core/repositories/users')
|
||||
const {
|
||||
addStreamInviteSentOutActivity
|
||||
} = require('@/modules/activitystream/services/streamActivityService')
|
||||
const {
|
||||
buildBasicTemplateEmail
|
||||
} = require('@/modules/emails/services/templateFormatting')
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
@@ -162,85 +165,12 @@ async function validateInput(params, inviter, targetUser, resource) {
|
||||
* @param {string} message
|
||||
* @returns {string}
|
||||
*/
|
||||
function sanitizeMessage(message) {
|
||||
function sanitizeMessage(message, stripAll = false) {
|
||||
return sanitizeHtml(message, {
|
||||
allowedTags: ['b', 'i', 'em', 'strong']
|
||||
allowedTags: stripAll ? [] : [('b', 'i', 'em', 'strong')]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build email text version body
|
||||
*/
|
||||
function buildEmailTextBody(invite, inviter, serverInfo, inviteLink, resourceName) {
|
||||
const { message } = invite
|
||||
const forServer = isServerInvite(invite)
|
||||
|
||||
const dynamicText = forServer
|
||||
? `join the ${serverInfo.name} Speckle Server (${process.env.CANONICAL_URL})`
|
||||
: `become a collaborator on the ${serverInfo.name} Speckle Server (${process.env.CANONICAL_URL}) stream - "${resourceName}"`
|
||||
|
||||
return `
|
||||
Hello!
|
||||
|
||||
${
|
||||
inviter.name
|
||||
} has just sent you this invitation to ${dynamicText}! To accept the invitation, open the following URL in your browser:
|
||||
${inviteLink}
|
||||
|
||||
${message ? inviter.name + ' said: "' + message + '"' : ''}
|
||||
|
||||
Warm regards,
|
||||
Speckle
|
||||
---
|
||||
This email was sent from ${serverInfo.name} at ${
|
||||
process.env.CANONICAL_URL
|
||||
}, deployed and managed by ${serverInfo.company}. Your admin contact is ${
|
||||
serverInfo.adminContact ? serverInfo.adminContact : '[not provided]'
|
||||
}.
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build email HTML version body
|
||||
*/
|
||||
function buildEmailHtmlBody(invite, inviter, serverInfo, inviteLink, resourceName) {
|
||||
const { message } = invite
|
||||
const forServer = isServerInvite(invite)
|
||||
|
||||
const dynamicText = forServer
|
||||
? `join the <a href="${process.env.CANONICAL_URL}" rel="notrack">${serverInfo.name} Speckle Server</a>`
|
||||
: `become a collaborator on the <a href="${process.env.CANONICAL_URL}" rel="notrack">${serverInfo.name} Speckle Server</a> stream - "${resourceName}"`
|
||||
|
||||
return `
|
||||
Hello!
|
||||
<br>
|
||||
<br>
|
||||
${inviter.name} has just sent you this invitation to ${dynamicText}!
|
||||
To accept the invitation, <a href="${inviteLink}" rel="notrack">click here</a>!
|
||||
|
||||
<br>
|
||||
<br>
|
||||
${message ? inviter.name + ' said: <em>"' + message + '"</em><br><br>' : ''}
|
||||
|
||||
Warm regards,
|
||||
<br>
|
||||
Speckle (on behalf of ${inviter.name})
|
||||
<br>
|
||||
<img src="https://speckle.systems/content/images/2021/02/logo_big-1.png" style="width:30px; height:30px;">
|
||||
<br>
|
||||
<br>
|
||||
<caption style="size:8px; color:#7F7F7F; width:400px; text-align: left;">
|
||||
This email was sent from ${serverInfo.name} at <a href="${
|
||||
process.env.CANONICAL_URL
|
||||
}" rel="notrack">${process.env.CANONICAL_URL}</a>, deployed and managed by ${
|
||||
serverInfo.company
|
||||
}. Your admin contact is ${
|
||||
serverInfo.adminContact ? serverInfo.adminContact : '[not provided]'
|
||||
}.
|
||||
</caption>
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the email subject line
|
||||
* @param {import('@/modules/serverinvites/repositories').ServerInviteRecord} invite
|
||||
@@ -287,6 +217,77 @@ function buildInviteLink(invite) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildHtmlPreamble(invite, inviter, serverInfo, resourceName) {
|
||||
const { message } = invite
|
||||
const forServer = isServerInvite(invite)
|
||||
|
||||
const dynamicText = forServer
|
||||
? `join the <b>${serverInfo.name}</b> Speckle Server`
|
||||
: `become a collaborator on the <b>${resourceName}</b> stream`
|
||||
|
||||
const bodyStart = `
|
||||
Hello!
|
||||
<br />
|
||||
<br />
|
||||
${inviter.name} has just sent you this invitation to ${dynamicText}!
|
||||
${message ? inviter.name + ' said: <em>"' + message + '"</em>' : ''}`
|
||||
|
||||
return {
|
||||
bodyStart,
|
||||
bodyEnd: 'Feel free to ignore this invite if you do not know the person sending it.'
|
||||
}
|
||||
}
|
||||
|
||||
function buildTextPreamble(invite, inviter, serverInfo, resourceName) {
|
||||
const { message } = invite
|
||||
const forServer = isServerInvite(invite)
|
||||
|
||||
const dynamicText = forServer
|
||||
? `join the ${serverInfo.name} Speckle Server`
|
||||
: `become a collaborator on the "${resourceName}" stream`
|
||||
|
||||
const bodyStart = `Hello!
|
||||
|
||||
${inviter.name} has just sent you this invitation to ${dynamicText}!
|
||||
|
||||
${message ? inviter.name + ' said: "' + sanitizeMessage(message, true) + '"' : ''}`
|
||||
|
||||
return {
|
||||
bodyStart,
|
||||
bodyEnd: 'Feel free to ignore this invite if you do not know the person sending it.'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@/modules/serverinvites/repositories').ServerInviteRecord} invite
|
||||
* @param {import('@/modules/core/helpers/userHelper').UserRecord} inviter
|
||||
* @param {{name: string, company: string, adminContact: string}} serverInfo
|
||||
* @param {string} resourceName
|
||||
* @returns {import('@/modules/emails/services/templateFormatting').BasicEmailTemplateParams}
|
||||
*/
|
||||
function buildEmailTemplateParams(
|
||||
invite,
|
||||
inviter,
|
||||
serverInfo,
|
||||
inviteLink,
|
||||
resourceName
|
||||
) {
|
||||
return {
|
||||
html: buildHtmlPreamble(invite, inviter, serverInfo, resourceName),
|
||||
text: buildTextPreamble(invite, inviter, serverInfo, resourceName),
|
||||
cta: {
|
||||
title: 'Accept the invitation',
|
||||
url: inviteLink
|
||||
},
|
||||
server: {
|
||||
name: serverInfo.name,
|
||||
url: process.env.CANONICAL_URL,
|
||||
company: serverInfo.company,
|
||||
contact: serverInfo.adminContact
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build invite email contents
|
||||
* @param {import('@/modules/serverinvites/repositories').ServerInviteRecord} invite
|
||||
@@ -301,27 +302,21 @@ async function buildEmailContents(invite, inviter, targetUser, resource) {
|
||||
const inviteLink = buildInviteLink(invite)
|
||||
const resourceName = resolveResourceName(invite, resource)
|
||||
|
||||
const bodyText = buildEmailTextBody(
|
||||
invite,
|
||||
inviter,
|
||||
serverInfo,
|
||||
inviteLink,
|
||||
resourceName
|
||||
)
|
||||
const bodyHtml = buildEmailHtmlBody(
|
||||
const templateParams = buildEmailTemplateParams(
|
||||
invite,
|
||||
inviter,
|
||||
serverInfo,
|
||||
inviteLink,
|
||||
resourceName
|
||||
)
|
||||
const { html, text } = await buildBasicTemplateEmail(templateParams)
|
||||
const subject = buildEmailSubject(invite, inviter, resourceName)
|
||||
|
||||
return {
|
||||
to: email,
|
||||
subject,
|
||||
text: bodyText,
|
||||
html: bodyHtml
|
||||
text,
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
import { Express } from 'express'
|
||||
|
||||
export type Nullable<T> = T | null
|
||||
export type Optional<T> = T | undefined
|
||||
export type MaybeAsync<T> = T | Promise<T>
|
||||
|
||||
export type SpeckleModule = {
|
||||
init: (app: Express) => MaybeAsync<void>
|
||||
finalize: (app: Express) => MaybeAsync<void>
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"dataloader": "^2.0.0",
|
||||
"debug": "^4.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.17.3",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.17.1",
|
||||
@@ -92,6 +93,7 @@
|
||||
"@swc/core": "^1.2.222",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/mocha": "^7.0.2",
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"search.useParentIgnoreFiles": true,
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
|
||||
@@ -5467,6 +5467,7 @@ __metadata:
|
||||
"@swc/core": ^1.2.222
|
||||
"@types/compression": ^1.7.2
|
||||
"@types/debug": ^4.1.7
|
||||
"@types/ejs": ^3.1.1
|
||||
"@types/express": ^4.17.13
|
||||
"@types/lodash": ^4.14.180
|
||||
"@types/mocha": ^7.0.2
|
||||
@@ -5497,6 +5498,7 @@ __metadata:
|
||||
debug: ^4.3.1
|
||||
deep-equal-in-any-order: ^1.1.15
|
||||
dotenv: ^8.2.0
|
||||
ejs: ^3.1.8
|
||||
eslint: ^8.11.0
|
||||
eslint-config-prettier: ^8.5.0
|
||||
express: ^4.17.3
|
||||
@@ -6195,6 +6197,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ejs@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "@types/ejs@npm:3.1.1"
|
||||
checksum: 12fa444920aaa70af2fae4424fa62b49c23b31a37129c428b7c9f9068e58c0696b4e5601b0449f87bae8794e039c679a43651c356c34f17d1bb460456dd41441
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/eslint-scope@npm:^3.7.3":
|
||||
version: 3.7.3
|
||||
resolution: "@types/eslint-scope@npm:3.7.3"
|
||||
@@ -10623,7 +10632,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
||||
"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
@@ -13238,6 +13247,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ejs@npm:^3.1.8":
|
||||
version: 3.1.8
|
||||
resolution: "ejs@npm:3.1.8"
|
||||
dependencies:
|
||||
jake: ^10.8.5
|
||||
bin:
|
||||
ejs: bin/cli.js
|
||||
checksum: 1d40d198ad52e315ccf37e577bdec06e24eefdc4e3c27aafa47751a03a0c7f0ec4310254c9277a5f14763c3cd4bbacce27497332b2d87c74232b9b1defef8efc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-to-chromium@npm:^1.4.118":
|
||||
version: 1.4.134
|
||||
resolution: "electron-to-chromium@npm:1.4.134"
|
||||
@@ -14748,6 +14768,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"filelist@npm:^1.0.1":
|
||||
version: 1.0.4
|
||||
resolution: "filelist@npm:1.0.4"
|
||||
dependencies:
|
||||
minimatch: ^5.0.1
|
||||
checksum: a303573b0821e17f2d5e9783688ab6fbfce5d52aaac842790ae85e704a6f5e4e3538660a63183d6453834dedf1e0f19a9dadcebfa3e926c72397694ea11f5160
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"filename-reserved-regex@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "filename-reserved-regex@npm:2.0.0"
|
||||
@@ -18115,6 +18144,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jake@npm:^10.8.5":
|
||||
version: 10.8.5
|
||||
resolution: "jake@npm:10.8.5"
|
||||
dependencies:
|
||||
async: ^3.2.3
|
||||
chalk: ^4.0.2
|
||||
filelist: ^1.0.1
|
||||
minimatch: ^3.0.4
|
||||
bin:
|
||||
jake: ./bin/cli.js
|
||||
checksum: 56c913ecf5a8d74325d0af9bc17a233bad50977438d44864d925bb6c45c946e0fee8c4c1f5fe2225471ef40df5222e943047982717ebff0d624770564d3c46ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"javascript-stringify@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "javascript-stringify@npm:1.6.0"
|
||||
|
||||
Reference in New Issue
Block a user