Merge pull request #1711 from Human-Connection/407-change_your_email_address

407 change your email address
This commit is contained in:
Robert Schäfer 2019-10-02 17:43:19 +02:00 committed by GitHub
commit cfdf9dad2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 2253 additions and 1448 deletions

View File

@ -5,7 +5,8 @@ import {
signupTemplate, signupTemplate,
resetPasswordTemplate, resetPasswordTemplate,
wrongAccountTemplate, wrongAccountTemplate,
} from './templates/templateBuilder' emailVerificationTemplate,
} from './templateBuilder'
const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
@ -57,8 +58,17 @@ const sendPasswordResetMail = async (resolve, root, args, context, resolveInfo)
return true return true
} }
const sendEmailVerificationMail = async (resolve, root, args, context, resolveInfo) => {
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce, name } = response
await sendMail(emailVerificationTemplate({ email, nonce, name }))
delete response.nonce
return response
}
export default { export default {
Mutation: { Mutation: {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail, requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail, Signup: sendSignupMail,
SignupByInvitation: sendSignupMail, SignupByInvitation: sendSignupMail,

View File

@ -0,0 +1,77 @@
import mustache from 'mustache'
import CONFIG from '../../config'
import * as templates from './templates'
const from = '"Human Connection" <info@human-connection.org>'
const supportUrl = 'https://human-connection.org/en/contact'
export const signupTemplate = ({ email, nonce }) => {
const subject = 'Willkommen, Bienvenue, Welcome to Human Connection!'
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl, subject },
{ content: templates.signup },
),
}
}
export const emailVerificationTemplate = ({ email, nonce, name }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.emailVerification },
),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
const subject = 'Neues Passwort | Reset Password'
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, name, nonce, supportUrl, subject },
{ content: templates.passwordReset },
),
}
}
export const wrongAccountTemplate = ({ email }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
from,
to: email,
subject,
html: mustache.render(
templates.layout,
{ actionUrl, supportUrl },
{ content: templates.wrongAccount },
),
}
}

View File

@ -0,0 +1,190 @@
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }}!</h1>
<p style="margin: 0;">Du möchtest also deine E-Mail ändern? Kein Problem! Mit Klick auf diesen Button
kannst Du Deine neue E-Mail Adresse bestätigen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">E-Mail
Adresse
bestätigen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Falls Du deine E-Mail Adresse doch nicht ändern möchtest, kannst du diese Nachricht
einfach ignorieren. Mlde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn du noch Fragen hast!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein Human Connection Team</p>
</td>
</tr>
<tr>
<td style="display: none;">
<p></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->
<!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td style="padding: 20px 0; text-align: center">
</td>
</tr>
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LcGvGRsW6DrZn7FWRzF%2F-LcGv6EiVcsjYLfQ_2YE%2F-LcGv8UtmAWc61fxGveg%2Flets_get_together.png?generation=1555078880410873&alt=media"
width="600" height="" alt="Human Connection community logo" border="0"
style="width: 100%; max-width: 600px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }}!</h1>
<p style="margin: 0;">So, you want to change your e-mail? No problem! Just click the button below to verify
your new address:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Verify
e-mail address</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-bottom: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't want to change your e-mail address feel free to ignore this message. You
can
also <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> if you have any questions!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The Human Connection Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -0,0 +1,11 @@
import fs from 'fs'
import path from 'path'
const readFile = fileName => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
export const signup = readFile('./signup.html')
export const passwordReset = readFile('./resetPassword.html')
export const wrongAccount = readFile('./wrongAccount.html')
export const emailVerification = readFile('./emailVerification.html')
export const layout = readFile('./layout.html')

View File

@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>{{ subject }}</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
{{> content}}
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@ -1,229 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Neues Passwort | Reset Password</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN --> <!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -258,8 +35,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -299,8 +75,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in <p style="margin: 0;">Sollte der Button für Dich nicht funktionieren, kannst Du auch folgenden Code in
Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p> Dein Browserfenster kopieren: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org" <p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
@ -322,8 +97,8 @@
<!-- Email Body German : END --> <!-- Email Body German : END -->
<!-- Email Body English : BEGIN --> <!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -358,8 +133,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -398,8 +172,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If the above button doesn't work you can also copy the following code into your <p style="margin: 0;">If the above button doesn't work you can also copy the following code into your
browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p> browser window: <span style="color: #17b53e;">{{{ nonce }}}</span></p>
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org" <p style="margin: 0; margin-top: 10px;">See you soon on <a href="https://human-connection.org"
@ -414,35 +187,3 @@
</table> </table>
<!-- Email Body English : END --> <!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@ -1,239 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Willkommen, Bienvenue, Welcome to Human Connection</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<!-- VISUALLY HIDDEN PRE-HEADER TEXT -->
<div
style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
Dein Anmeldelink. | Here is your signup link.
</div>
<div
style="display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;">
&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;<br>
</div>
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN --> <!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -269,8 +36,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -297,8 +63,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Falls Du Dich nicht selbst bei <a href="https://human-connection.org" <p style="margin: 0;">Falls Du Dich nicht selbst bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei! style="color: #17b53e;">Human Connection</a> angemeldet hast, schau doch mal vorbei!
Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.</p> Wir sind ein gemeinnütziges Aktionsnetzwerk von Menschen für Menschen.</p>
@ -321,8 +86,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei <p style="margin: 0;">Melde Dich gerne <a href="{{{ supportUrl }}}" style="color: #17b53e;">bei
unserem Support Team</a>, wenn Du Fragen hast.</p> unserem Support Team</a>, wenn Du Fragen hast.</p>
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org" <p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="https://human-connection.org"
@ -344,8 +108,8 @@
<!-- Email Body German : END --> <!-- Email Body German : END -->
<!-- Email Body English : BEGIN --> <!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -381,8 +145,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -409,8 +172,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you didn't sign up for <a href="https://human-connection.org" <p style="margin: 0;">If you didn't sign up for <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> we recommend you to check it out! style="color: #17b53e;">Human Connection</a> we recommend you to check it out!
It's a social network from people for people who want to connect and change the world together.</p> It's a social network from people for people who want to connect and change the world together.</p>
@ -434,8 +196,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our <p style="margin: 0;">Feel free to <a href="{{{ supportUrl }}}" style="color: #17b53e;">contact our
support team</a> with any support team</a> with any
questions you have.</p> questions you have.</p>
@ -451,35 +212,3 @@
</table> </table>
<!-- Email Body English : END --> <!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@ -1,48 +0,0 @@
import fs from 'fs'
import path from 'path'
import mustache from 'mustache'
import CONFIG from '../../../config'
const from = '"Human Connection" <info@human-connection.org>'
const supportUrl = 'https://human-connection.org/en/contact'
const signupHtml = fs.readFileSync(path.join(__dirname, './signup.html'), 'utf-8')
const passwordResetHtml = fs.readFileSync(path.join(__dirname, './resetPassword.html'), 'utf-8')
const wrongAccountHtml = fs.readFileSync(path.join(__dirname, './wrongAccount.html'), 'utf-8')
export const signupTemplate = ({ email, nonce }) => {
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject: 'Willkommen, Bienvenue, Welcome to Human Connection!',
html: mustache.render(signupHtml, { actionUrl, supportUrl }),
}
}
export const resetPasswordTemplate = ({ email, nonce, name }) => {
const actionUrl = new URL('/password-reset/change-password', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
return {
from,
to: email,
subject: 'Neues Passwort | Reset Password',
html: mustache.render(passwordResetHtml, { actionUrl, name, nonce, supportUrl }),
}
}
export const wrongAccountTemplate = ({ email }) => {
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
return {
from,
to: email,
subject: 'Falsche Mailadresse? | Wrong E-mail?',
html: mustache.render(wrongAccountHtml, { actionUrl, supportUrl }),
}
}

View File

@ -1,229 +1,6 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
<title>Falsche Mailadresse? | Wrong E-mail?</title>
<!--[if mso]>
<style>
* {
font-family: sans-serif !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
<!--<![endif]-->
<!-- CSS RESETS -->
<style>
html,
body {
margin: 0 !important;
padding: 0 !important;
height: 100% !important;
width: 100% !important;
}
* {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
div[style*="margin: 16px 0"] {
margin: 0 !important;
}
table,
td {
mso-table-lspace: 0pt !important;
mso-table-rspace: 0pt !important;
}
table {
border-spacing: 0 !important;
border-collapse: collapse !important;
table-layout: fixed !important;
margin: 0 auto !important;
}
img {
-ms-interpolation-mode: bicubic;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a[x-apple-data-detectors],
.unstyle-auto-detected-links a,
.aBn {
border-bottom: 0 !important;
cursor: default !important;
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
.a6S {
display: none !important;
opacity: 0.01 !important;
}
.im {
color: inherit !important;
}
img.g-img+div {
display: none !important;
}
/* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
@media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
u~div .email-container {
min-width: 320px !important;
}
}
/* iPhone 6, 6S, 7, 8, and X */
@media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
u~div .email-container {
min-width: 375px !important;
}
}
/* iPhone 6+, 7+, and 8+ */
@media only screen and (min-device-width: 414px) {
u~div .email-container {
min-width: 414px !important;
}
}
</style>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!-- PROGRESSIVE ENHANCEMENTS -->
<style>
.button-td,
.button-a {
transition: all 100ms ease-in;
}
.button-td-primary:hover,
.button-a-primary:hover {
background: #19c243 !important;
border-color: #555555 !important;
}
@media screen and (max-width: 600px) {
.email-container p {
font-size: 17px !important;
}
}
</style>
<!-- LANGUAGE TOGGLE -->
<style>
.toggle+label {
display: inline-block;
height: 40px;
padding: 0 12px;
margin-top: 40px;
line-height: 38px;
font-family: Lato, sans-serif;
font-size: 16px;
border: 1px solid #cbc7d1;
cursor: pointer;
}
.toggle+label:hover {
background-color: #bee876;
}
.toggle:checked+label {
background-color: #19c243;
color: white;
}
.toggle-english+label {
border-bottom-right-radius: 50px;
border-top-right-radius: 50px;
border-left: none;
margin-left: -2px;
}
.toggle-german+label {
border-bottom-left-radius: 50px;
border-top-left-radius: 50px;
border-right: none;
margin-right: -2px;
}
.toggle-german:checked~table.email-german {
display: block;
}
.toggle-german:checked~table.email-english {
display: none;
}
.toggle-english:checked~table.email-english {
display: block;
}
.toggle-english:checked~table.email-german {
display: none;
}
</style>
</head>
<body width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #f5f4f6;">
<center style="width: 100%; background-color: #f5f4f6;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f5f4f6;">
<tr>
<td>
<![endif]-->
<div style="max-width: 600px; margin: 0 auto;" class="email-container">
<!--[if mso]>
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
<tr>
<td>
<![endif]-->
<!-- LANGUAGE TOGGLE -->
<input type="radio" name="language" class="toggle toggle-german" style="display: none;" id="toggle-german"
checked="checked">
<label for="toggle-german">Deutsch</label>
<input type="radio" name="language" class="toggle toggle-english" style="display:none;" id="toggle-english">
<label for="toggle-english">English</label>
<p style="margin: 0;"></p>
<!-- Email Body German : BEGIN --> <!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -259,8 +36,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -282,8 +58,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">Wenn Du noch keinen Account bei <a href="https://human-connection.org" <p style="margin: 0;">Wenn Du noch keinen Account bei <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst, style="color: #17b53e;">Human Connection</a> hast oder Dein Password gar nicht ändern willst,
kannst Du diese E-Mail einfach ignorieren!</p> kannst Du diese E-Mail einfach ignorieren!</p>
@ -322,8 +97,8 @@
<!-- Email Body German : END --> <!-- Email Body German : END -->
<!-- Email Body English : BEGIN --> <!-- Email Body English : BEGIN -->
<table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table class="email-english" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
width="100%" style="margin: auto;"> style="margin: auto;">
<tr> <tr>
<td style="padding: 20px 0; text-align: center"> <td style="padding: 20px 0; text-align: center">
</td> </td>
@ -358,8 +133,7 @@
<tr> <tr>
<td style="padding: 0 20px;"> <td style="padding: 0 20px;">
<!-- Button : BEGIN --> <!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
style="margin: auto;">
<tr> <tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;"> <td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}" <a class="button-a button-a-primary" href="{{{ actionUrl }}}"
@ -381,8 +155,7 @@
<td style="background-color: #ffffff; padding: 0 20px;"> <td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"> <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr> <tr>
<td <td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0;">If you don't have an account at <a href="https://human-connection.org" <p style="margin: 0;">If you don't have an account at <a href="https://human-connection.org"
style="color: #17b53e;">Human Connection</a> yet or if you didn't want to reset your password, style="color: #17b53e;">Human Connection</a> yet or if you didn't want to reset your password,
please ignore this e-mail.</p> please ignore this e-mail.</p>
@ -414,35 +187,3 @@
</table> </table>
<!-- Email Body English : END --> <!-- Email Body English : END -->
<!-- Email Footer : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
Human Connection gGmbH<br><span class="unstyle-auto-detected-links">Bahnhofstraße 11, 73235 Weilheim /
Teck<br>Germany</span>
<br><br>
</td>
</tr>
</table>
<!-- Email Footer : END -->
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</center>
</body>
</html>

View File

@ -170,6 +170,8 @@ const permissions = shield(
block: isAuthenticated, block: isAuthenticated,
unblock: isAuthenticated, unblock: isAuthenticated,
markAsRead: isAuthenticated, markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,
}, },
User: { User: {
email: isMyOwn, email: isMyOwn,

View File

@ -0,0 +1,12 @@
module.exports = {
email: { type: 'string', primary: true, lowercase: true, email: true },
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
nonce: { type: 'string', token: true },
belongsTo: {
type: 'relationship',
relationship: 'BELONGS_TO',
target: 'User',
direction: 'out',
eager: true,
},
}

View File

@ -5,6 +5,7 @@ export default {
User: require('./User.js'), User: require('./User.js'),
InvitationCode: require('./InvitationCode.js'), InvitationCode: require('./InvitationCode.js'),
EmailAddress: require('./EmailAddress.js'), EmailAddress: require('./EmailAddress.js'),
UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'),
SocialMedia: require('./SocialMedia.js'), SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'), Post: require('./Post.js'),
Comment: require('./Comment.js'), Comment: require('./Comment.js'),

View File

@ -0,0 +1,92 @@
import generateNonce from './helpers/generateNonce'
import Resolver from './helpers/Resolver'
import existingEmailAddress from './helpers/existingEmailAddress'
import { UserInputError } from 'apollo-server'
import Validator from 'neode/build/Services/Validator.js'
export default {
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response
try {
const { neode } = context
await new Validator(neode, neode.model('UnverifiedEmailAddress'), args)
} catch (e) {
throw new UserInputError('must be a valid email')
}
// check email does not belong to anybody
await existingEmailAddress(_parent, args, context)
const nonce = generateNonce()
const {
user: { id: userId },
} = context
const { email } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async txc => {
const result = await txc.run(
`
MATCH (user:User {id: $userId})
MERGE (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce})
SET email.createdAt = toString(datetime())
RETURN email, user
`,
{ userId, email, nonce },
)
return result.records.map(record => ({
name: record.get('user').properties.name,
...record.get('email').properties,
}))
})
try {
const txResult = await writeTxResultPromise
response = txResult[0]
} finally {
session.close()
}
return response
},
VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response
const {
user: { id: userId },
} = context
const { nonce, email } = args
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async txc => {
const result = await txc.run(
`
MATCH (user:User {id: $userId})-[:PRIMARY_EMAIL]->(previous:EmailAddress)
MATCH (user)<-[:BELONGS_TO]-(email:UnverifiedEmailAddress {email: $email, nonce: $nonce})
MERGE (user)-[:PRIMARY_EMAIL]->(email)
SET email:EmailAddress
SET email.verifiedAt = toString(datetime())
REMOVE email:UnverifiedEmailAddress
DETACH DELETE previous
RETURN email
`,
{ userId, email, nonce },
)
return result.records.map(record => record.get('email').properties)
})
try {
const txResult = await writeTxResultPromise
response = txResult[0]
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('A user account with this email already exists.')
throw new Error(e)
} finally {
session.close()
}
if (!response) throw new UserInputError('Invalid nonce or no email address found.')
return response
},
},
EmailAddress: {
...Resolver('EmailAddress', {
undefinedToNull: ['verifiedAt'],
}),
},
}

View File

@ -0,0 +1,298 @@
import Factory from '../../seed/factories'
import { gql } from '../../jest/helpers'
import { getDriver, neode as getNeode } from '../../bootstrap/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
const factory = Factory()
const neode = getNeode()
let mutate
let authenticatedUser
let user
let variables
const driver = getDriver()
beforeEach(async () => {
variables = {}
})
beforeAll(() => {
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
})
afterEach(async () => {
await factory.cleanDatabase()
})
describe('AddEmailAddress', () => {
const mutation = gql`
mutation($email: String!) {
AddEmailAddress(email: $email) {
email
verifiedAt
createdAt
}
}
`
beforeEach(() => {
variables = { ...variables, email: 'new-email@example.org' }
})
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
})
it('throws AuthorizationError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
describe('email attribute is not a valid email', () => {
beforeEach(() => {
variables = { ...variables, email: 'foobar' }
})
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'must be a valid email' }],
})
})
})
describe('email attribute is a valid email', () => {
it('creates a new unverified `EmailAddress` node', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
AddEmailAddress: {
email: 'new-email@example.org',
verifiedAt: null,
createdAt: expect.any(String),
},
},
errors: undefined,
})
})
it('connects `UnverifiedEmailAddress` to the authenticated user', async () => {
await mutate({ mutation, variables })
const result = await neode.cypher(`
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"})
MATCH(u:User)<-[:BELONGS_TO]-(e:UnverifiedEmailAddress {email: "new-email@example.org"})
RETURN e
`)
const email = neode.hydrateFirst(result, 'e', neode.model('UnverifiedEmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'new-email@example.org',
nonce: expect.any(String),
})
})
describe('if another `UnverifiedEmailAddress` node already exists with that email', () => {
it('throws no unique constraint violation error', async () => {
await factory.create('UnverifiedEmailAddress', {
createdAt: '2019-09-24T14:00:01.565Z',
email: 'new-email@example.org',
})
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
AddEmailAddress: {
email: 'new-email@example.org',
verifiedAt: null,
},
},
errors: undefined,
})
})
})
describe('but if another user owns an `EmailAddress` already with that email', () => {
it('throws UserInputError because of unique constraints', async () => {
await factory.create('User', { email: 'new-email@example.org' })
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { AddEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
})
})
})
describe('VerifyEmailAddress', () => {
const mutation = gql`
mutation($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
createdAt
verifiedAt
}
}
`
beforeEach(() => {
variables = { ...variables, email: 'to-be-verified@example.org', nonce: '123456' }
})
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
})
it('throws AuthorizationError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Not Authorised!' }],
})
})
})
describe('authenticated', () => {
beforeEach(async () => {
user = await factory.create('User', { id: '567', email: 'user@example.org' })
authenticatedUser = await user.toJson()
})
describe('if no unverified `EmailAddress` node exists', () => {
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('given a `UnverifiedEmailAddress`', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await factory.create('UnverifiedEmailAddress', {
nonce: 'abcdef',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
})
})
describe('given invalid nonce', () => {
it('throws UserInputError', async () => {
variables.nonce = 'asdfgh'
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('given valid nonce for `UnverifiedEmailAddress` node', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' }
})
describe('but the address does not belong to the authenticated user', () => {
it('throws UserInputError', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'Invalid nonce or no email address found.' }],
})
})
})
describe('and the `UnverifiedEmailAddress` belongs to the authenticated user', () => {
beforeEach(async () => {
await emailAddress.relateTo(user, 'belongsTo')
})
it('adds `verifiedAt`', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: {
VerifyEmailAddress: {
email: 'to-be-verified@example.org',
verifiedAt: expect.any(String),
createdAt: expect.any(String),
},
},
errors: undefined,
})
})
it('connects the new `EmailAddress` as PRIMARY', async () => {
await mutate({ mutation, variables })
const result = await neode.cypher(`
MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"})
RETURN e
`)
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'to-be-verified@example.org',
})
})
it('removes previous PRIMARY relationship', async () => {
const cypherStatement = `
MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"})
RETURN e
`
let result = await neode.cypher(cypherStatement)
let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'user@example.org',
})
await mutate({ mutation, variables })
result = await neode.cypher(cypherStatement)
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false)
})
it('removes previous `EmailAddress` node', async () => {
const cypherStatement = `
MATCH(u:User {id: "567"})<-[:BELONGS_TO]-(e:EmailAddress {email: "user@example.org"})
RETURN e
`
let result = await neode.cypher(cypherStatement)
let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email.toJson()).resolves.toMatchObject({
email: 'user@example.org',
})
await mutate({ mutation, variables })
result = await neode.cypher(cypherStatement)
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
await expect(email).toBe(false)
})
describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => {
beforeEach(async () => {
await factory.create('EmailAddress', { email: 'to-be-verified@example.org' })
})
it('throws UserInputError because of unique constraints', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { VerifyEmailAddress: null },
errors: [{ message: 'A user account with this email already exists.' }],
})
})
})
})
})
})
})
})

View File

@ -0,0 +1,26 @@
import { UserInputError } from 'apollo-server'
export default async function alreadyExistingMail(_parent, args, context) {
let { email } = args
email = email.toLowerCase()
const cypher = `
MATCH (email:EmailAddress {email: $email})
OPTIONAL MATCH (email)-[:BELONGS_TO]-(user)
RETURN email, user
`
let transactionRes
const session = context.driver.session()
try {
transactionRes = await session.run(cypher, { email })
} finally {
session.close()
}
const [result] = transactionRes.records.map(record => {
return {
alreadyExistingEmail: record.get('email').properties,
user: record.get('user') && record.get('user').properties,
}
})
const { alreadyExistingEmail, user } = result || {}
if (user) throw new UserInputError('A user account with this email already exists.')
return alreadyExistingEmail
}

View File

@ -0,0 +1,4 @@
import uuid from 'uuid/v4'
export default function generateNonce() {
return uuid().substring(0, 6)
}

View File

@ -1,41 +1,16 @@
import { UserInputError } from 'apollo-server' import { UserInputError } from 'apollo-server'
import uuid from 'uuid/v4'
import { neode } from '../../bootstrap/neo4j' import { neode } from '../../bootstrap/neo4j'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
import encryptPassword from '../../helpers/encryptPassword' import encryptPassword from '../../helpers/encryptPassword'
import generateNonce from './helpers/generateNonce'
import existingEmailAddress from './helpers/existingEmailAddress'
const instance = neode() const instance = neode()
const alreadyExistingMail = async (_parent, args, context) => {
let { email } = args
email = email.toLowerCase()
const cypher = `
MATCH (email:EmailAddress {email: $email})
OPTIONAL MATCH (email)-[:PRIMARY_EMAIL]-(user)
RETURN email, user
`
let transactionRes
const session = context.driver.session()
try {
transactionRes = await session.run(cypher, { email })
} finally {
session.close()
}
const [result] = transactionRes.records.map(record => {
return {
alreadyExistingEmail: record.get('email').properties,
user: record.get('user') && record.get('user').properties,
}
})
const { alreadyExistingEmail, user } = result || {}
if (user) throw new UserInputError('User account with this email already exists.')
return alreadyExistingEmail
}
export default { export default {
Mutation: { Mutation: {
CreateInvitationCode: async (_parent, args, context, _resolveInfo) => { CreateInvitationCode: async (_parent, args, context, _resolveInfo) => {
args.token = uuid().substring(0, 6) args.token = generateNonce()
const { const {
user: { id: userId }, user: { id: userId },
} = context } = context
@ -54,9 +29,9 @@ export default {
return response return response
}, },
Signup: async (_parent, args, context) => { Signup: async (_parent, args, context) => {
const nonce = uuid().substring(0, 6) const nonce = generateNonce()
args.nonce = nonce args.nonce = nonce
let emailAddress = await alreadyExistingMail(_parent, args, context) let emailAddress = await existingEmailAddress(_parent, args, context)
if (emailAddress) return emailAddress if (emailAddress) return emailAddress
try { try {
emailAddress = await instance.create('EmailAddress', args) emailAddress = await instance.create('EmailAddress', args)
@ -67,9 +42,9 @@ export default {
}, },
SignupByInvitation: async (_parent, args, context) => { SignupByInvitation: async (_parent, args, context) => {
const { token } = args const { token } = args
const nonce = uuid().substring(0, 6) const nonce = generateNonce()
args.nonce = nonce args.nonce = nonce
let emailAddress = await alreadyExistingMail(_parent, args, context) let emailAddress = await existingEmailAddress(_parent, args, context)
if (emailAddress) return emailAddress if (emailAddress) return emailAddress
try { try {
const result = await instance.cypher( const result = await instance.cypher(

View File

@ -257,7 +257,7 @@ describe('SignupByInvitation', () => {
it('throws unique violation error', async () => { it('throws unique violation error', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'User account with this email already exists.' }], errors: [{ message: 'A user account with this email already exists.' }],
}) })
}) })
}) })
@ -307,6 +307,7 @@ describe('Signup', () => {
it('is allowed to signup users by email', async () => { it('is allowed to signup users by email', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } }, data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
}) })
}) })
@ -342,7 +343,7 @@ describe('Signup', () => {
it('throws UserInputError error because of unique constraint violation', async () => { it('throws UserInputError error because of unique constraint violation', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'User account with this email already exists.' }], errors: [{ message: 'A user account with this email already exists.' }],
}) })
}) })
}) })
@ -351,6 +352,7 @@ describe('Signup', () => {
it('resolves with the already existing email', async () => { it('resolves with the already existing email', async () => {
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } }, data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
}) })
}) })
@ -359,6 +361,7 @@ describe('Signup', () => {
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2) await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { Signup: { email: 'someuser@example.org' } }, data: { Signup: { email: 'someuser@example.org' } },
errors: undefined,
}) })
await expect(neode.all('EmailAddress')).resolves.toHaveLength(2) await expect(neode.all('EmailAddress')).resolves.toHaveLength(2)
}) })

View File

@ -20,4 +20,9 @@ type Mutation {
about: String about: String
termsAndConditionsAgreedVersion: String! termsAndConditionsAgreedVersion: String!
): User ): User
AddEmailAddress(email: String!): EmailAddress
VerifyEmailAddress(
nonce: String!
email: String!
): EmailAddress
} }

View File

@ -1,8 +1,6 @@
import faker from 'faker' import faker from 'faker'
export default function create() { export function defaults({ args }) {
return {
factory: async ({ args, neodeInstance }) => {
const defaults = { const defaults = {
email: faker.internet.email(), email: faker.internet.email(),
verifiedAt: new Date().toISOString(), verifiedAt: new Date().toISOString(),
@ -11,6 +9,13 @@ export default function create() {
...defaults, ...defaults,
...args, ...args,
} }
return args
}
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('EmailAddress', args) return neodeInstance.create('EmailAddress', args)
}, },
} }

View File

@ -9,6 +9,7 @@ import createTag from './tags.js'
import createSocialMedia from './socialMedia.js' import createSocialMedia from './socialMedia.js'
import createLocation from './locations.js' import createLocation from './locations.js'
import createEmailAddress from './emailAddresses.js' import createEmailAddress from './emailAddresses.js'
import createUnverifiedEmailAddresss from './unverifiedEmailAddresses.js'
export const seedServerHost = 'http://127.0.0.1:4001' export const seedServerHost = 'http://127.0.0.1:4001'
@ -32,6 +33,7 @@ const factories = {
SocialMedia: createSocialMedia, SocialMedia: createSocialMedia,
Location: createLocation, Location: createLocation,
EmailAddress: createEmailAddress, EmailAddress: createEmailAddress,
UnverifiedEmailAddress: createUnverifiedEmailAddresss,
} }
export const cleanDatabase = async (options = {}) => { export const cleanDatabase = async (options = {}) => {

View File

@ -0,0 +1,10 @@
import { defaults } from './emailAddresses.js'
export default function create() {
return {
factory: async ({ args, neodeInstance }) => {
args = defaults({ args })
return neodeInstance.create('UnverifiedEmailAddress', args)
},
}
}

View File

@ -115,7 +115,7 @@ describe('Signup', () => {
mocks.$apollo.mutate = jest mocks.$apollo.mutate = jest
.fn() .fn()
.mockRejectedValue( .mockRejectedValue(
new Error('UserInputError: User account with this email already exists.'), new Error('UserInputError: A user account with this email already exists.'),
) )
}) })

View File

@ -128,7 +128,7 @@ export default {
} catch (err) { } catch (err) {
const { message } = err const { message } = err
const mapping = { const mapping = {
'User account with this email already exists': 'email-exists', 'A user account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token', 'Invitation code already used or does not exist': 'invalid-invitation-token',
} }
for (const [pattern, key] of Object.entries(mapping)) { for (const [pattern, key] of Object.entries(mapping)) {

View File

@ -0,0 +1,20 @@
import gql from 'graphql-tag'
export const AddEmailAddressMutation = gql`
mutation($email: String!) {
AddEmailAddress(email: $email) {
email
createdAt
}
}
`
export const VerifyEmailAddressMutation = gql`
mutation($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
verifiedAt
createdAt
}
}
`

View File

@ -158,6 +158,27 @@
"labelBio": "Über dich", "labelBio": "Über dich",
"success": "Deine Daten wurden erfolgreich aktualisiert!" "success": "Deine Daten wurden erfolgreich aktualisiert!"
}, },
"email": {
"validation": {
"same-email": "Das ist deine aktuelle E-Mail Addresse"
},
"name": "Deine E-Mail",
"labelEmail": "E-Mail Adresse ändern",
"labelNewEmail": "Neue E-Mail Adresse",
"labelNonce": "Bestätigungscode eingeben",
"success": "Eine neue E-Mail Addresse wurde registriert.",
"submitted": "Eine E-Mail zur Bestätigung deiner Adresse wurde an <b>{email}</b> gesendet.",
"change-successful": "Deine E-Mail Adresse wurde erfolgreich geändert.",
"verification-error": {
"message": "Deine E-Mail Adresse konnte nicht verifiziert werden.",
"support": "Wenn das Problem weiterhin besteht, kontaktiere uns gerne per E-Mail an",
"explanation": "Das kann verschiedene Ursachen haben:",
"reason": {
"invalid-nonce": "Ist der Bestätigungscode falsch?",
"no-email-request": "Bist du dir sicher, dass du eine Änderung deiner E-Mail Adresse angefragt hattest?"
}
}
},
"validation": { "validation": {
"slug": { "slug": {
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt.", "regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt.",

View File

@ -159,6 +159,27 @@
"labelBio": "About You", "labelBio": "About You",
"success": "Your data was successfully updated!" "success": "Your data was successfully updated!"
}, },
"email": {
"validation": {
"same-email": "This is your current email address"
},
"name": "Your email",
"labelEmail": "Change your email address",
"labelNewEmail": "New email Address",
"labelNonce": "Enter your code",
"success": "A new email address has been registered.",
"submitted": "An email to verify your address has been sent to <b>{email}</b>.",
"change-successful": "Your email address has been changed successfully.",
"verification-error": {
"message": "Your email could not be changed.",
"explanation": "This can have different causes:",
"reason": {
"invalid-nonce": "Is the confirmation code invalid?",
"no-email-request": "Are you certain that you requested a change of your email address?"
},
"support": "If the problem persists, please contact us by email at"
}
},
"validation": { "validation": {
"slug": { "slug": {
"regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.", "regex": "Allowed characters are only lowercase letters, numbers, underscores and hyphens.",
@ -254,7 +275,7 @@
"users": { "users": {
"name": "Users", "name": "Users",
"form": { "form": {
"placeholder": "E-Mail, name or description" "placeholder": "email, name or description"
}, },
"table": { "table": {
"columns": { "columns": {

View File

@ -23,6 +23,10 @@ export default {
name: this.$t('settings.data.name'), name: this.$t('settings.data.name'),
path: `/settings`, path: `/settings`,
}, },
{
name: this.$t('settings.email.name'),
path: `/settings/my-email-address`,
},
{ {
name: this.$t('settings.security.name'), name: this.$t('settings.security.name'),
path: `/settings/security`, path: `/settings/security`,

View File

@ -31,14 +31,7 @@
:placeholder="$t('settings.data.labelBio')" :placeholder="$t('settings.data.labelBio')"
/> />
<template slot="footer"> <template slot="footer">
<ds-button <ds-button icon="check" :disabled="errors" type="submit" :loading="loadingData" primary>
style="float: right;"
icon="check"
:disabled="errors"
type="submit"
:loading="loadingData"
primary
>
{{ $t('actions.save') }} {{ $t('actions.save') }}
</ds-button> </ds-button>
</template> </template>

View File

@ -0,0 +1,53 @@
import { mount, createLocalVue } from '@vue/test-utils'
import EnterNoncePage from './enter-nonce.vue'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('EnterNoncePage', () => {
let mocks
let wrapper
beforeEach(() => {
wrapper = null
mocks = {
$t: jest.fn(t => t),
$route: {
query: {},
},
$router: {
replace: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(EnterNoncePage, {
mocks,
localVue,
})
}
describe('form', () => {
describe('submit', () => {
it('renders form errors', () => {
wrapper = Wrapper()
wrapper.find('form').trigger('submit')
expect(mocks.$router.replace).not.toHaveBeenCalled()
})
describe('entering a nonce', () => {
it('redirects to my-email-address/verify', () => {
wrapper = Wrapper()
wrapper.find('#nonce').setValue('foobar')
wrapper.find('form').trigger('submit')
expect(mocks.$router.replace).toHaveBeenCalled()
})
})
})
})
})
})

View File

@ -0,0 +1,59 @@
<template>
<ds-form v-model="form" :schema="formSchema" @submit="submit">
<template slot-scope="{ errors }">
<ds-card :header="$t('settings.email.name')">
<ds-input
id="email"
model="email"
icon="envelope"
disabled
:label="$t('settings.email.labelNewEmail')"
/>
<ds-input
id="nonce"
model="nonce"
icon="question-circle"
:label="$t('settings.email.labelNonce')"
/>
<template slot="footer">
<ds-button class="submit-button" icon="check" :disabled="errors" type="submit" primary>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
export default {
data() {
return {
formSchema: {
nonce: { type: 'string', required: true },
},
}
},
computed: {
form: {
get: function() {
const { email = '', nonce = '' } = this.$route.query
return { email, nonce }
},
set: function(formData) {
this.formData = formData
},
},
},
methods: {
async submit() {
const { email, nonce } = this.formData
this.$router.replace({
path: 'verify',
query: { email, nonce },
})
},
},
}
</script>

View File

@ -0,0 +1,116 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import EmailSettingsIndexPage from './index.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
describe('EmailSettingsIndexPage', () => {
let store
let mocks
let wrapper
beforeEach(() => {
wrapper = null
store = new Vuex.Store({
getters: {
'auth/user': () => {
return { id: 'u23', email: 'some-mail@example.org' }
},
},
})
mocks = {
$t: jest.fn(t => t),
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
mutate: jest.fn().mockResolvedValue(),
},
$router: {
push: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(EmailSettingsIndexPage, {
store,
mocks,
localVue,
})
}
describe('form', () => {
describe('submit', () => {
beforeEach(jest.useFakeTimers)
describe('email unchanged', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('form').trigger('submit')
})
it('displays form errors', () => {
expect(wrapper.text()).not.toContain('settings.email.submitted')
expect(wrapper.text()).toContain('settings.email.validation.same-email')
})
it('does not call $apollo.mutate', () => {
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
})
describe('enter another email', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('#email').setValue('yet-another-email@example.org')
wrapper.find('form').trigger('submit')
})
it('calls $apollo.mutate', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
it('no form errors', () => {
expect(wrapper.text()).not.toContain('settings.email.validation.same-email')
expect(wrapper.text()).toContain('settings.email.submitted')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('redirects to `my-email-address/enter-nonce`', () => {
expect(mocks.$router.push).toHaveBeenCalledWith({
path: 'my-email-address/enter-nonce',
query: { email: 'yet-another-email@example.org' },
})
})
})
})
describe('if backend responds with unique constraint violation', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValue({
message: 'User account already exists',
})
wrapper = Wrapper()
wrapper.find('#email').setValue('already-taken@example.org')
wrapper.find('form').trigger('submit')
})
it('translates error message', () => {
expect(wrapper.text()).toContain('registration.signup.form.errors.email-exists')
})
})
})
})
})
})

View File

@ -0,0 +1,113 @@
<template>
<ds-card centered v-if="success">
<transition name="ds-transition-fade">
<sweetalert-icon icon="info" />
</transition>
<ds-text v-html="submitMessage" />
</ds-card>
<ds-form v-else v-model="form" :schema="formSchema" @submit="submit">
<template slot-scope="{ errors }">
<ds-card :header="$t('settings.email.name')">
<ds-input
id="email"
model="email"
icon="envelope"
:label="$t('settings.email.labelEmail')"
/>
<template slot="footer">
<ds-space class="backendErrors" v-if="backendErrors">
<ds-text align="center" bold color="danger">{{ backendErrors.message }}</ds-text>
</ds-space>
<ds-button icon="check" :disabled="errors" type="submit" primary>
{{ $t('actions.save') }}
</ds-button>
</template>
</ds-card>
</template>
</ds-form>
</template>
<script>
import { mapGetters } from 'vuex'
import { AddEmailAddressMutation } from '~/graphql/EmailAddress.js'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
data() {
return {
backendErrors: null,
success: false,
}
},
computed: {
submitMessage() {
const { email } = this.formData
return this.$t('settings.email.submitted', { email })
},
...mapGetters({
currentUser: 'auth/user',
}),
form: {
get: function() {
const { email } = this.currentUser
return { email }
},
set: function(formData) {
this.formData = formData
},
},
formSchema() {
const { email } = this.currentUser
const sameEmailValidationError = this.$t('settings.email.validation.same-email')
return {
email: [
{ type: 'email', required: true },
{
validator(rule, value, callback, source, options) {
const errors = []
if (email === value) {
errors.push(sameEmailValidationError)
}
return errors
},
},
],
}
},
},
methods: {
async submit() {
const { email } = this.formData
try {
await this.$apollo.mutate({
mutation: AddEmailAddressMutation,
variables: { email },
})
this.$toast.success(this.$t('settings.email.success'))
this.success = true
setTimeout(() => {
this.$router.push({
path: 'my-email-address/enter-nonce',
query: { email },
})
}, 3000)
} catch (err) {
if (err.message.includes('exists')) {
// We cannot use form validation errors here, the backend does not
// have a query to filter for email addresses. This is a privacy
// consideration. We could implement a dedicated query to check that
// but I think it's too much effort for this feature.
this.backendErrors = { message: this.$t('registration.signup.form.errors.email-exists') }
return
}
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -0,0 +1,164 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import EmailVerifyPage from './verify.vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
describe('EmailVerifyPage', () => {
let store
let mocks
let wrapper
let setUser
beforeEach(() => {
setUser = jest.fn()
wrapper = null
store = new Vuex.Store({
getters: {
'auth/user': () => {
return { id: 'u23', email: 'some-mail@example.org' }
},
},
mutations: {
'auth/SET_USER': setUser,
},
})
mocks = {
$t: jest.fn(t => t),
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$router: {
replace: jest.fn(),
},
store,
}
})
describe('asyncData', () => {
const asyncDataAction = () => {
const context = {
store: mocks.store,
query: {},
app: {
apolloProvider: {
defaultClient: mocks.$apollo,
},
},
}
return EmailVerifyPage.asyncData(context)
}
describe('backend sends successful response', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: {
VerifyEmailAddress: {
email: 'verified-email@example.org',
},
},
}),
},
}
})
it('sets `success` to true', async () => {
await expect(asyncDataAction()).resolves.toEqual({
success: true,
})
})
it("updates current user's email", async () => {
await asyncDataAction()
expect(setUser).toHaveBeenCalledWith({}, { id: 'u23', email: 'verified-email@example.org' })
})
})
describe('backend sends unsuccessful response', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest.fn().mockRejectedValue({
data: { VerifyEmailAddress: null },
errors: [{ message: 'User account already exists with that email' }],
}),
},
}
})
it('sets `success` to false', async () => {
await expect(asyncDataAction()).resolves.toEqual({
success: false,
})
})
it('does not updates current user', async () => {
await asyncDataAction()
expect(setUser).not.toHaveBeenCalled()
})
})
})
describe('mount', () => {
beforeEach(jest.useFakeTimers)
const Wrapper = () => {
return mount(EmailVerifyPage, {
store,
mocks,
localVue,
})
}
describe('given successful verification', () => {
beforeEach(() => {
mocks = { ...mocks, success: true }
wrapper = Wrapper()
})
it('shows success message', () => {
expect(wrapper.text()).toContain('settings.email.change-successful')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('redirects to email settings page', () => {
expect(mocks.$router.replace).toHaveBeenCalledWith({
name: 'settings-my-email-address',
})
})
})
})
describe('given unsuccessful verification', () => {
beforeEach(() => {
mocks = { ...mocks, success: false }
wrapper = Wrapper()
})
it('shows success message', () => {
expect(wrapper.text()).toContain('settings.email.verification-error')
})
describe('after timeout', () => {
beforeEach(jest.runAllTimers)
it('does not redirect', () => {
expect(mocks.$router.replace).not.toHaveBeenCalledWith()
})
})
})
})
})

View File

@ -0,0 +1,99 @@
<template>
<ds-card>
<transition name="ds-transition-fade">
<client-only>
<sweetalert-icon :icon="sweetAlertIcon" />
</client-only>
</transition>
<ds-space v-if="success">
<ds-text bold align="center">
{{ $t(`settings.email.change-successful`) }}
</ds-text>
</ds-space>
<template v-else>
<ds-text bold align="center">
{{ $t(`settings.email.verification-error.message`) }}
</ds-text>
<ds-space class="message">
<client-only>
<ds-text>
<ds-space margin-top="large" margin-bottom="small">
{{ $t(`settings.email.verification-error.explanation`) }}
</ds-space>
<ds-list>
<ds-list-item>
{{ $t(`settings.email.verification-error.reason.invalid-nonce`) }}
</ds-list-item>
<ds-list-item>
{{ $t(`settings.email.verification-error.reason.no-email-request`) }}
</ds-list-item>
</ds-list>
{{ $t('settings.email.verification-error.support') }}
<a href="mailto:support@human-connection.org">support@human-connection.org</a>
</ds-text>
</client-only>
</ds-space>
</template>
</ds-card>
</template>
<script>
import { VerifyEmailAddressMutation } from '~/graphql/EmailAddress.js'
import { SweetalertIcon } from 'vue-sweetalert-icons'
export default {
components: {
SweetalertIcon,
},
computed: {
sweetAlertIcon() {
return this.success ? 'success' : 'error'
},
},
created() {
if (this.success) {
setTimeout(() => {
this.$router.replace({ name: 'settings-my-email-address' })
}, 3000)
}
},
async asyncData(context) {
const {
store,
query,
app: { apolloProvider },
} = context
const client = apolloProvider.defaultClient
let success
const { email = '', nonce = '' } = query
const currentUser = store.getters['auth/user']
try {
const response = await client.mutate({
mutation: VerifyEmailAddressMutation,
variables: { email, nonce },
})
const {
data: { VerifyEmailAddress },
} = response
success = true
store.commit(
'auth/SET_USER',
{ ...currentUser, email: VerifyEmailAddress.email },
{ root: true },
)
} catch (error) {
success = false
}
return { success }
},
}
</script>
<style lang="scss" scoped>
.message {
display: flex;
justify-content: space-around;
}
</style>