API - init user account activation
This commit is contained in:
parent
b5b4ac8f92
commit
a1f80e9745
@ -41,3 +41,13 @@ def password_change_email(user: Dict, email_data: Dict) -> None:
|
|||||||
recipient=user['email'],
|
recipient=user['email'],
|
||||||
data=email_data,
|
data=email_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dramatiq.actor(queue_name='fittrackee_emails')
|
||||||
|
def account_confirmation_email(user: Dict, email_data: Dict) -> None:
|
||||||
|
email_service.send(
|
||||||
|
template='account_confirmation',
|
||||||
|
lang=user['language'],
|
||||||
|
recipient=user['email'],
|
||||||
|
data=email_data,
|
||||||
|
)
|
||||||
|
266
fittrackee/emails/templates/account_confirmation/en/body.html
Normal file
266
fittrackee/emails/templates/account_confirmation/en/body.html
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="x-apple-disable-message-reformatting" content=""/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>FitTrackee - Confirm your account</title>
|
||||||
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
color: #51545E;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3869D4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a img {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
font-size: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #51545E;
|
||||||
|
margin: .4em 0 1.1875em;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sub {
|
||||||
|
color: #6B6E76;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #3869D4;
|
||||||
|
border-top: 10px solid #3869D4;
|
||||||
|
border-right: 18px solid #3869D4;
|
||||||
|
border-bottom: 10px solid #3869D4;
|
||||||
|
border-left: 18px solid #3869D4;
|
||||||
|
display: inline-block;
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--green {
|
||||||
|
background-color: #22BC66;
|
||||||
|
border-top: 10px solid #22BC66;
|
||||||
|
border-right: 18px solid #22BC66;
|
||||||
|
border-bottom: 10px solid #22BC66;
|
||||||
|
border-left: 18px solid #22BC66;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
.button {
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead {
|
||||||
|
padding: 25px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #A8AAAF;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px 0 white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body-inner {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-action {
|
||||||
|
width: 100%;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-sub {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-cell {
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-body-inner,
|
||||||
|
.email-footer {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body,
|
||||||
|
.email-body,
|
||||||
|
.email-body-inner,
|
||||||
|
.email-content,
|
||||||
|
.email-wrapper,
|
||||||
|
.email-masthead,
|
||||||
|
.email-footer {
|
||||||
|
background-color: #333333 !important;
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
p,
|
||||||
|
h1 {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
.email-masthead-name {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
.f-fallback {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span class="preheader">Use this link to confirm your account.</span>
|
||||||
|
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="email-masthead">
|
||||||
|
<a href="{{fittrackee_url}}" class="f-fallback email-masthead-name">
|
||||||
|
FitTrackee
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Hi {{username}},</h1>
|
||||||
|
<p>You have created an account on FitTrackee account. Use the link below to confirm your address email.</p>
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="{{account_confirmation_url}}" class="f-fallback button button--green" target="_blank">Verify your email</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
{% if operating_system and browser_name %}For security, this request was received from a {{operating_system}} device using {{browser_name}}.
|
||||||
|
{% endif %}If this account creation wasn't initiated by you, please ignore this email.
|
||||||
|
</p>
|
||||||
|
<p>Thanks,
|
||||||
|
<br>The FitTrackee Team</p>
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
|
<p class="f-fallback sub">{{account_confirmation_url}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell" align="center">
|
||||||
|
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
fittrackee/emails/templates/account_confirmation/en/body.txt
Normal file
12
fittrackee/emails/templates/account_confirmation/en/body.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Hi {{username}},
|
||||||
|
|
||||||
|
You have created an account on FitTrackee account. Use the link below to confirm your address email.
|
||||||
|
|
||||||
|
Verify your email: {{ account_confirmation_url }}
|
||||||
|
|
||||||
|
{% if operating_system and browser_name %}For security, this request was received from a {{operating_system}} device using {{browser_name}}.
|
||||||
|
{% endif %}If this account creation wasn't initiated by you, please ignore this email.
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The FitTrackee Team
|
||||||
|
{{fittrackee_url}}
|
@ -0,0 +1 @@
|
|||||||
|
FitTrackee - Confirm your account
|
268
fittrackee/emails/templates/account_confirmation/fr/body.html
Normal file
268
fittrackee/emails/templates/account_confirmation/fr/body.html
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="x-apple-disable-message-reformatting" content=""/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<title>FitTrackee - Confirmer votre inscription</title>
|
||||||
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
color: #51545E;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #3869D4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a img {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preheader {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden;
|
||||||
|
mso-hide: all;
|
||||||
|
font-size: 1px;
|
||||||
|
line-height: 1px;
|
||||||
|
max-height: 0;
|
||||||
|
max-width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #51545E;
|
||||||
|
margin: .4em 0 1.1875em;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.625;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.sub {
|
||||||
|
color: #6B6E76;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #3869D4;
|
||||||
|
border-top: 10px solid #3869D4;
|
||||||
|
border-right: 18px solid #3869D4;
|
||||||
|
border-bottom: 10px solid #3869D4;
|
||||||
|
border-left: 18px solid #3869D4;
|
||||||
|
display: inline-block;
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--green {
|
||||||
|
background-color: #22BC66;
|
||||||
|
border-top: 10px solid #22BC66;
|
||||||
|
border-right: 18px solid #22BC66;
|
||||||
|
border-bottom: 10px solid #22BC66;
|
||||||
|
border-left: 18px solid #22BC66;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
.button {
|
||||||
|
width: 100% !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #F4F4F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead {
|
||||||
|
padding: 25px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-masthead-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #A8AAAF;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px 0 white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body-inner {
|
||||||
|
width: 570px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-action {
|
||||||
|
width: 100%;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-sub {
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 1px solid #EAEAEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-cell {
|
||||||
|
padding: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.email-body-inner,
|
||||||
|
.email-footer {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body,
|
||||||
|
.email-body,
|
||||||
|
.email-body-inner,
|
||||||
|
.email-content,
|
||||||
|
.email-wrapper,
|
||||||
|
.email-masthead,
|
||||||
|
.email-footer {
|
||||||
|
background-color: #333333 !important;
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
p,
|
||||||
|
h1 {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
.email-masthead-name {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<style type="text/css">
|
||||||
|
.f-fallback {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span class="preheader">Utiliser ce lien pour confirmer votre inscription.</span>
|
||||||
|
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="email-masthead">
|
||||||
|
<a href="{{fittrackee_url}}" class="f-fallback email-masthead-name">
|
||||||
|
FitTrackee
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Bonjour {{username}},</h1>
|
||||||
|
<p>Vous avez créé un sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour confirmer votre adresse email.
|
||||||
|
</p>
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="{{account_confirmation_url}}" class="f-fallback button button--green" target="_blank">Vérifier l'adresse email</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
{% if operating_system and browser_name %}Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||||
|
{% endif %}Si vous n'êtes pas à l'origine de la création de ce compte, vous pouvez ignorer cet e-mail.
|
||||||
|
</p>
|
||||||
|
<p>Merci,
|
||||||
|
<br>L'équipe FitTrackee</p>
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">Si vous avez des problèmes avec le bouton, vous pouvez copier et coller le lien suivant dans votre navigateur</p>
|
||||||
|
<p class="f-fallback sub">{{account_confirmation_url}}</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell" align="center">
|
||||||
|
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
fittrackee/emails/templates/account_confirmation/fr/body.txt
Normal file
13
fittrackee/emails/templates/account_confirmation/fr/body.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Bonjour {{username}},
|
||||||
|
|
||||||
|
Vous avez créé un sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour confirmer votre adresse email.
|
||||||
|
|
||||||
|
Vérifier l'adresse email : {{ account_confirmation_url }}
|
||||||
|
|
||||||
|
{% if operating_system and browser_name %}Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||||
|
{% endif %}Si vous n'êtes pas à l'origine de la création de ce compte, vous pouvez ignorer cet e-mail.
|
||||||
|
|
||||||
|
Merci,
|
||||||
|
L'équipe FitTrackee
|
||||||
|
{{fittrackee_url}}
|
@ -0,0 +1 @@
|
|||||||
|
FitTrackee - Confirmer votre inscription
|
@ -25,6 +25,11 @@ def upgrade():
|
|||||||
'users', 'email', existing_type=sa.String(length=120),
|
'users', 'email', existing_type=sa.String(length=120),
|
||||||
type_=sa.String(length=255), existing_nullable=False
|
type_=sa.String(length=255), existing_nullable=False
|
||||||
)
|
)
|
||||||
|
op.add_column(
|
||||||
|
'users',
|
||||||
|
sa.Column('is_active', sa.Boolean(), default=False, nullable=True))
|
||||||
|
op.execute("UPDATE users SET is_active = true")
|
||||||
|
op.alter_column('users', 'is_active', nullable=False)
|
||||||
op.add_column(
|
op.add_column(
|
||||||
'users',
|
'users',
|
||||||
sa.Column('email_to_confirm', sa.String(length=255), nullable=True))
|
sa.Column('email_to_confirm', sa.String(length=255), nullable=True))
|
||||||
@ -36,6 +41,7 @@ def upgrade():
|
|||||||
def downgrade():
|
def downgrade():
|
||||||
op.drop_column('users', 'confirmation_token')
|
op.drop_column('users', 'confirmation_token')
|
||||||
op.drop_column('users', 'email_to_confirm')
|
op.drop_column('users', 'email_to_confirm')
|
||||||
|
op.drop_column('users', 'is_active')
|
||||||
op.alter_column(
|
op.alter_column(
|
||||||
'users', 'email', existing_type=sa.String(length=255),
|
'users', 'email', existing_type=sa.String(length=255),
|
||||||
type_=sa.String(length=120), existing_nullable=False
|
type_=sa.String(length=120), existing_nullable=False
|
||||||
|
@ -9,17 +9,12 @@ from ..mixins import ApiTestCaseMixin
|
|||||||
|
|
||||||
|
|
||||||
class TestGetConfig(ApiTestCaseMixin):
|
class TestGetConfig(ApiTestCaseMixin):
|
||||||
def test_it_gets_application_config(
|
def test_it_gets_application_config_for_unauthenticated_user(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask
|
||||||
) -> None:
|
) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client = app.test_client()
|
||||||
app, user_1.email
|
|
||||||
)
|
|
||||||
|
|
||||||
response = client.get(
|
response = client.get('/api/config')
|
||||||
'/api/config',
|
|
||||||
headers=dict(Authorization=f'Bearer {auth_token}'),
|
|
||||||
)
|
|
||||||
|
|
||||||
data = json.loads(response.data.decode())
|
data = json.loads(response.data.decode())
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@ -36,6 +31,22 @@ class TestGetConfig(ApiTestCaseMixin):
|
|||||||
)
|
)
|
||||||
assert data['data']['version'] == fittrackee.__version__
|
assert data['data']['version'] == fittrackee.__version__
|
||||||
|
|
||||||
|
def test_it_gets_application_config(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
'/api/config',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'success' in data['status']
|
||||||
|
|
||||||
def test_it_returns_error_if_application_has_no_config(
|
def test_it_returns_error_if_application_has_no_config(
|
||||||
self, app_no_config: Flask, user_1_admin: User
|
self, app_no_config: Flask, user_1_admin: User
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
expected_en_text_body = """Hi test,
|
||||||
|
|
||||||
|
You have created an account on FitTrackee account. Use the link below to confirm your address email.
|
||||||
|
|
||||||
|
Verify your email: http://localhost/account-confirmation?token=xxx
|
||||||
|
|
||||||
|
For security, this request was received from a Linux device using Firefox.
|
||||||
|
If this account creation wasn't initiated by you, please ignore this email.
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The FitTrackee Team
|
||||||
|
http://localhost"""
|
||||||
|
|
||||||
|
expected_fr_text_body = """Bonjour test,
|
||||||
|
|
||||||
|
Vous avez créé un sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour confirmer votre adresse email.
|
||||||
|
|
||||||
|
Vérifier l'adresse email : http://localhost/account-confirmation?token=xxx
|
||||||
|
|
||||||
|
Pour vérification, cette demande a été reçue à partir d'un appareil sous Linux, utilisant le navigateur Firefox.
|
||||||
|
Si vous n'êtes pas à l'origine de la création de ce compte, vous pouvez ignorer cet e-mail.
|
||||||
|
|
||||||
|
Merci,
|
||||||
|
L'équipe FitTrackee
|
||||||
|
http://localhost"""
|
||||||
|
|
||||||
|
expected_en_html_body = """ <body>
|
||||||
|
<span class="preheader">Use this link to confirm your account.</span>
|
||||||
|
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="email-masthead">
|
||||||
|
<a href="http://localhost" class="f-fallback email-masthead-name">
|
||||||
|
FitTrackee
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Hi test,</h1>
|
||||||
|
<p>You have created an account on FitTrackee account. Use the link below to confirm your address email.</p>
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="http://localhost/account-confirmation?token=xxx" class="f-fallback button button--green" target="_blank">Verify your email</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
For security, this request was received from a Linux device using Firefox.
|
||||||
|
If this account creation wasn't initiated by you, please ignore this email.
|
||||||
|
</p>
|
||||||
|
<p>Thanks,
|
||||||
|
<br>The FitTrackee Team</p>
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
|
<p class="f-fallback sub">http://localhost/account-confirmation?token=xxx</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell" align="center">
|
||||||
|
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
expected_fr_html_body = """ <body>
|
||||||
|
<span class="preheader">Utiliser ce lien pour confirmer votre inscription.</span>
|
||||||
|
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="email-masthead">
|
||||||
|
<a href="http://localhost" class="f-fallback email-masthead-name">
|
||||||
|
FitTrackee
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell">
|
||||||
|
<div class="f-fallback">
|
||||||
|
<h1>Bonjour test,</h1>
|
||||||
|
<p>Vous avez créé un sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour confirmer votre adresse email.
|
||||||
|
</p>
|
||||||
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="http://localhost/account-confirmation?token=xxx" class="f-fallback button button--green" target="_blank">Vérifier l'adresse email</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
Pour vérification, cette demande a été reçue à partir d'un appareil sous Linux, utilisant le navigateur Firefox.
|
||||||
|
Si vous n'êtes pas à l'origine de la création de ce compte, vous pouvez ignorer cet e-mail.
|
||||||
|
</p>
|
||||||
|
<p>Merci,
|
||||||
|
<br>L'équipe FitTrackee</p>
|
||||||
|
<table class="body-sub" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p class="f-fallback sub">Si vous avez des problèmes avec le bouton, vous pouvez copier et coller le lien suivant dans votre navigateur</p>
|
||||||
|
<p class="f-fallback sub">http://localhost/account-confirmation?token=xxx</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<td class="content-cell" align="center">
|
||||||
|
<p class="f-fallback sub align-center">© FitTrackee.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
77
fittrackee/tests/emails/test_email_account_confirmation.py
Normal file
77
fittrackee/tests/emails/test_email_account_confirmation.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
from fittrackee.emails.email import EmailTemplate
|
||||||
|
|
||||||
|
from .template_results.email_account_confirmation import (
|
||||||
|
expected_en_html_body,
|
||||||
|
expected_en_text_body,
|
||||||
|
expected_fr_html_body,
|
||||||
|
expected_fr_text_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailTemplateForAccountConfirmation:
|
||||||
|
EMAIL_DATA = {
|
||||||
|
'username': 'test',
|
||||||
|
'account_confirmation_url': (
|
||||||
|
'http://localhost/account-confirmation?token=xxx'
|
||||||
|
),
|
||||||
|
'operating_system': 'Linux',
|
||||||
|
'browser_name': 'Firefox',
|
||||||
|
'fittrackee_url': 'http://localhost',
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'lang, expected_subject',
|
||||||
|
[
|
||||||
|
('en', 'FitTrackee - Confirm your account'),
|
||||||
|
('fr', 'FitTrackee - Confirmer votre inscription'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_it_gets_subject(
|
||||||
|
self, app: Flask, lang: str, expected_subject: str
|
||||||
|
) -> None:
|
||||||
|
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
|
||||||
|
|
||||||
|
subject = email_template.get_content(
|
||||||
|
'account_confirmation', lang, 'subject.txt', {}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert subject == expected_subject
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'lang, expected_text_body',
|
||||||
|
[
|
||||||
|
('en', expected_en_text_body),
|
||||||
|
('fr', expected_fr_text_body),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_it_gets_text_body(
|
||||||
|
self, app: Flask, lang: str, expected_text_body: str
|
||||||
|
) -> None:
|
||||||
|
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'account_confirmation', lang, 'body.txt', self.EMAIL_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert text_body == expected_text_body
|
||||||
|
|
||||||
|
def test_it_gets_en_html_body(self, app: Flask) -> None:
|
||||||
|
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'account_confirmation', 'en', 'body.html', self.EMAIL_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_en_html_body in text_body
|
||||||
|
|
||||||
|
def test_it_gets_fr_html_body(self, app: Flask) -> None:
|
||||||
|
email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'account_confirmation', 'fr', 'body.html', self.EMAIL_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_fr_html_body in text_body
|
6
fittrackee/tests/fixtures/fixtures_emails.py
vendored
6
fittrackee/tests/fixtures/fixtures_emails.py
vendored
@ -46,3 +46,9 @@ def user_reset_password_email() -> Iterator[MagicMock]:
|
|||||||
def user_email_updated_to_new_address_mock() -> Iterator[MagicMock]:
|
def user_email_updated_to_new_address_mock() -> Iterator[MagicMock]:
|
||||||
with patch('fittrackee.users.users.email_updated_to_new_address') as mock:
|
with patch('fittrackee.users.users.email_updated_to_new_address') as mock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def account_confirmation_email_mock() -> Iterator[MagicMock]:
|
||||||
|
with patch('fittrackee.users.auth.account_confirmation_email') as mock:
|
||||||
|
yield mock
|
||||||
|
18
fittrackee/tests/fixtures/fixtures_users.py
vendored
18
fittrackee/tests/fixtures/fixtures_users.py
vendored
@ -10,6 +10,7 @@ from fittrackee.workouts.models import Sport
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_1() -> User:
|
def user_1() -> User:
|
||||||
user = User(username='test', email='test@test.com', password='12345678')
|
user = User(username='test', email='test@test.com', password='12345678')
|
||||||
|
user.is_active = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
@ -18,6 +19,7 @@ def user_1() -> User:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_1_upper() -> User:
|
def user_1_upper() -> User:
|
||||||
user = User(username='TEST', email='TEST@TEST.COM', password='12345678')
|
user = User(username='TEST', email='TEST@TEST.COM', password='12345678')
|
||||||
|
user.is_active = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
@ -29,6 +31,7 @@ def user_1_admin() -> User:
|
|||||||
username='admin', email='admin@example.com', password='12345678'
|
username='admin', email='admin@example.com', password='12345678'
|
||||||
)
|
)
|
||||||
admin.admin = True
|
admin.admin = True
|
||||||
|
admin.is_active = True
|
||||||
db.session.add(admin)
|
db.session.add(admin)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return admin
|
return admin
|
||||||
@ -44,6 +47,7 @@ def user_1_full() -> User:
|
|||||||
user.language = 'en'
|
user.language = 'en'
|
||||||
user.timezone = 'America/New_York'
|
user.timezone = 'America/New_York'
|
||||||
user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y')
|
user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y')
|
||||||
|
user.is_active = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
@ -53,6 +57,7 @@ def user_1_full() -> User:
|
|||||||
def user_1_paris() -> User:
|
def user_1_paris() -> User:
|
||||||
user = User(username='test', email='test@test.com', password='12345678')
|
user = User(username='test', email='test@test.com', password='12345678')
|
||||||
user.timezone = 'Europe/Paris'
|
user.timezone = 'Europe/Paris'
|
||||||
|
user.is_active = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
@ -61,6 +66,7 @@ def user_1_paris() -> User:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_2() -> User:
|
def user_2() -> User:
|
||||||
user = User(username='toto', email='toto@toto.com', password='12345678')
|
user = User(username='toto', email='toto@toto.com', password='12345678')
|
||||||
|
user.is_active = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
@ -69,6 +75,7 @@ def user_2() -> User:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_2_admin() -> User:
|
def user_2_admin() -> User:
|
||||||
user = User(username='toto', email='toto@toto.com', password='12345678')
|
user = User(username='toto', email='toto@toto.com', password='12345678')
|
||||||
|
user.is_active = True
|
||||||
user.admin = True
|
user.admin = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -78,12 +85,23 @@ def user_2_admin() -> User:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_3() -> User:
|
def user_3() -> User:
|
||||||
user = User(username='sam', email='sam@test.com', password='12345678')
|
user = User(username='sam', email='sam@test.com', password='12345678')
|
||||||
|
user.is_active = True
|
||||||
user.weekm = True
|
user.weekm = True
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def inactive_user() -> User:
|
||||||
|
user = User(
|
||||||
|
username='inactive', email='inactive@example.com', password='12345678'
|
||||||
|
)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def user_sport_1_preference(
|
def user_sport_1_preference(
|
||||||
user_1: User, sport_1_cycling: Sport
|
user_1: User, sport_1_cycling: Sport
|
||||||
|
@ -53,7 +53,10 @@ class ApiTestCaseMixin(RandomMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_401(response: TestResponse, error_message: str) -> Dict:
|
def assert_401(
|
||||||
|
response: TestResponse,
|
||||||
|
error_message: Optional[str] = 'provide a valid auth token',
|
||||||
|
) -> Dict:
|
||||||
return assert_errored_response(
|
return assert_errored_response(
|
||||||
response, 401, error_message=error_message
|
response, 401, error_message=error_message
|
||||||
)
|
)
|
||||||
|
@ -123,7 +123,7 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_400(response, 'sorry, that user already exists')
|
self.assert_400(response, 'sorry, that username is already taken')
|
||||||
|
|
||||||
def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
|
def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
@ -193,11 +193,111 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_400(response, 'email: valid email must be provided\n')
|
self.assert_400(response, 'email: valid email must be provided\n')
|
||||||
|
|
||||||
|
def test_it_does_not_send_email_after_error(
|
||||||
|
self, app: Flask, account_confirmation_email_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
username=self.random_string(),
|
||||||
|
email=self.random_string(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
account_confirmation_email_mock.send.assert_not_called()
|
||||||
|
|
||||||
|
def test_it_returns_success_if_payload_is_valid(self, app: Flask) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
username=self.random_string(),
|
||||||
|
email=self.random_email(),
|
||||||
|
password=self.random_string(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content_type == 'application/json'
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert 'auth_token' not in data
|
||||||
|
|
||||||
|
def test_it_creates_user_with_inactive_account(self, app: Flask) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
username = self.random_string()
|
||||||
|
email = self.random_email()
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password=self.random_string(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
new_user = User.query.filter_by(username=username).first()
|
||||||
|
assert new_user.email == email
|
||||||
|
assert new_user.password is not None
|
||||||
|
assert new_user.is_active is False
|
||||||
|
|
||||||
|
def test_it_calls_account_confirmation_email_if_payload_is_valid(
|
||||||
|
self, app: Flask, account_confirmation_email_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
email = self.random_email()
|
||||||
|
username = self.random_string()
|
||||||
|
expected_token = self.random_string()
|
||||||
|
|
||||||
|
with patch('secrets.token_urlsafe', return_value=expected_token):
|
||||||
|
client.post(
|
||||||
|
'/api/auth/register',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
password='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
environ_base={'HTTP_USER_AGENT': USER_AGENT},
|
||||||
|
)
|
||||||
|
|
||||||
|
account_confirmation_email_mock.send.assert_called_once_with(
|
||||||
|
{
|
||||||
|
'language': 'en',
|
||||||
|
'email': email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'username': username,
|
||||||
|
'fittrackee_url': 'http://0.0.0.0:5000',
|
||||||
|
'operating_system': 'linux',
|
||||||
|
'browser_name': 'firefox',
|
||||||
|
'account_confirmation_url': (
|
||||||
|
'http://0.0.0.0:5000/account-confirmation'
|
||||||
|
f'?token={expected_token}'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'text_transformation',
|
'text_transformation',
|
||||||
['upper', 'lower'],
|
['upper', 'lower'],
|
||||||
)
|
)
|
||||||
def test_it_returns_error_if_user_already_exists_with_same_email(
|
def test_it_does_not_return_error_if_a_user_already_exists_with_same_email(
|
||||||
self, app: Flask, user_1: User, text_transformation: str
|
self, app: Flask, user_1: User, text_transformation: str
|
||||||
) -> None:
|
) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
@ -217,29 +317,30 @@ class TestUserRegistration(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_400(response, 'sorry, that user already exists')
|
assert response.status_code == 200
|
||||||
|
assert response.content_type == 'application/json'
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert 'auth_token' not in data
|
||||||
|
|
||||||
def test_user_can_register(self, app: Flask) -> None:
|
def test_it_does_not_call_account_confirmation_email_if_user_already_exists( # noqa
|
||||||
|
self, app: Flask, user_1: User, account_confirmation_email_mock: Mock
|
||||||
|
) -> None:
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
|
|
||||||
response = client.post(
|
client.post(
|
||||||
'/api/auth/register',
|
'/api/auth/register',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
username=self.random_string(),
|
username=self.random_string(),
|
||||||
email=self.random_email(),
|
email=user_1.email,
|
||||||
password=self.random_string(),
|
password=self.random_string(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
account_confirmation_email_mock.send.assert_not_called()
|
||||||
assert response.content_type == 'application/json'
|
|
||||||
data = json.loads(response.data.decode())
|
|
||||||
assert data['status'] == 'success'
|
|
||||||
assert data['message'] == 'successfully registered'
|
|
||||||
assert data['auth_token']
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserLogin(ApiTestCaseMixin):
|
class TestUserLogin(ApiTestCaseMixin):
|
||||||
@ -269,6 +370,21 @@ class TestUserLogin(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_401(response, 'invalid credentials')
|
self.assert_401(response, 'invalid credentials')
|
||||||
|
|
||||||
|
def test_it_returns_error_if_user_account_is_inactive(
|
||||||
|
self, app: Flask, inactive_user: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/login',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(email=inactive_user.email, password='12345678')
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response, 'invalid credentials')
|
||||||
|
|
||||||
def test_it_returns_error_if_password_is_invalid(
|
def test_it_returns_error_if_password_is_invalid(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -1628,7 +1744,7 @@ class TestRegistrationConfiguration(ApiTestCaseMixin):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
class TestPasswordResetRequest(ApiTestCaseMixin):
|
class TestPasswordResetRequest(ApiTestCaseMixin):
|
||||||
@ -1974,3 +2090,47 @@ class TestEmailUpdateWitUnauthenticatedUser(ApiTestCaseMixin):
|
|||||||
assert user_1.email == new_email
|
assert user_1.email == new_email
|
||||||
assert user_1.email_to_confirm is None
|
assert user_1.email_to_confirm is None
|
||||||
assert user_1.confirmation_token is None
|
assert user_1.confirmation_token is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfirmationAccount(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/account/confirm',
|
||||||
|
data=json.dumps(dict()),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response)
|
||||||
|
|
||||||
|
def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/account/confirm',
|
||||||
|
data=json.dumps(dict(token=self.random_string())),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response)
|
||||||
|
|
||||||
|
def test_it_activates_user_account(
|
||||||
|
self, app: Flask, inactive_user: User
|
||||||
|
) -> None:
|
||||||
|
token = self.random_string()
|
||||||
|
inactive_user.confirmation_token = token
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/account/confirm',
|
||||||
|
data=json.dumps(dict(token=token)),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['message'] == 'account confirmation successful'
|
||||||
|
assert inactive_user.is_active is True
|
||||||
|
assert inactive_user.confirmation_token is None
|
||||||
|
@ -28,6 +28,41 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_403(response)
|
self.assert_403(response)
|
||||||
|
|
||||||
|
def test_it_gets_inactive_user(
|
||||||
|
self, app: Flask, user_1_admin: User, inactive_user: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1_admin.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f'/api/users/{inactive_user.username}',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert len(data['data']['users']) == 1
|
||||||
|
user = data['data']['users'][0]
|
||||||
|
assert user['username'] == inactive_user.username
|
||||||
|
assert user['email'] == inactive_user.email
|
||||||
|
assert user['created_at']
|
||||||
|
assert not user['admin']
|
||||||
|
assert not user['is_active']
|
||||||
|
assert user['first_name'] is None
|
||||||
|
assert user['last_name'] is None
|
||||||
|
assert user['birth_date'] is None
|
||||||
|
assert user['bio'] is None
|
||||||
|
assert user['location'] is None
|
||||||
|
assert user['nb_sports'] == 0
|
||||||
|
assert user['nb_workouts'] == 0
|
||||||
|
assert user['records'] == []
|
||||||
|
assert user['sports_list'] == []
|
||||||
|
assert user['total_distance'] == 0
|
||||||
|
assert user['total_duration'] == '0:00:00'
|
||||||
|
|
||||||
def test_it_gets_single_user_without_workouts(
|
def test_it_gets_single_user_without_workouts(
|
||||||
self, app: Flask, user_1_admin: User, user_2: User
|
self, app: Flask, user_1_admin: User, user_2: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -46,10 +81,11 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
assert data['status'] == 'success'
|
assert data['status'] == 'success'
|
||||||
assert len(data['data']['users']) == 1
|
assert len(data['data']['users']) == 1
|
||||||
user = data['data']['users'][0]
|
user = data['data']['users'][0]
|
||||||
assert user['username'] == 'toto'
|
assert user['username'] == user_2.username
|
||||||
assert user['email'] == 'toto@toto.com'
|
assert user['email'] == user_2.email
|
||||||
assert user['created_at']
|
assert user['created_at']
|
||||||
assert not user['admin']
|
assert not user['admin']
|
||||||
|
assert user['is_active']
|
||||||
assert user['first_name'] is None
|
assert user['first_name'] is None
|
||||||
assert user['last_name'] is None
|
assert user['last_name'] is None
|
||||||
assert user['birth_date'] is None
|
assert user['birth_date'] is None
|
||||||
@ -87,10 +123,11 @@ class TestGetUser(ApiTestCaseMixin):
|
|||||||
assert data['status'] == 'success'
|
assert data['status'] == 'success'
|
||||||
assert len(data['data']['users']) == 1
|
assert len(data['data']['users']) == 1
|
||||||
user = data['data']['users'][0]
|
user = data['data']['users'][0]
|
||||||
assert user['username'] == 'test'
|
assert user['username'] == user_1.username
|
||||||
assert user['email'] == 'test@test.com'
|
assert user['email'] == user_1.email
|
||||||
assert user['created_at']
|
assert user['created_at']
|
||||||
assert not user['admin']
|
assert not user['admin']
|
||||||
|
assert user['is_active']
|
||||||
assert user['first_name'] is None
|
assert user['first_name'] is None
|
||||||
assert user['last_name'] is None
|
assert user['last_name'] is None
|
||||||
assert user['birth_date'] is None
|
assert user['birth_date'] is None
|
||||||
@ -134,8 +171,8 @@ class TestGetUsers(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_403(response)
|
self.assert_403(response)
|
||||||
|
|
||||||
def test_it_get_users_list(
|
def test_it_get_users_list_regardless_their_account_status(
|
||||||
self, app: Flask, user_1_admin: User, user_2: User, user_3: User
|
self, app: Flask, user_1_admin: User, inactive_user: User, user_3: User
|
||||||
) -> None:
|
) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user_1_admin.email
|
app, user_1_admin.email
|
||||||
@ -154,11 +191,14 @@ class TestGetUsers(ApiTestCaseMixin):
|
|||||||
assert 'created_at' in data['data']['users'][1]
|
assert 'created_at' in data['data']['users'][1]
|
||||||
assert 'created_at' in data['data']['users'][2]
|
assert 'created_at' in data['data']['users'][2]
|
||||||
assert 'admin' in data['data']['users'][0]['username']
|
assert 'admin' in data['data']['users'][0]['username']
|
||||||
assert 'toto' in data['data']['users'][1]['username']
|
assert 'inactive' in data['data']['users'][1]['username']
|
||||||
assert 'sam' in data['data']['users'][2]['username']
|
assert 'sam' in data['data']['users'][2]['username']
|
||||||
assert 'admin@example.com' in data['data']['users'][0]['email']
|
assert 'admin@example.com' in data['data']['users'][0]['email']
|
||||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
assert 'inactive@example.com' in data['data']['users'][1]['email']
|
||||||
assert 'sam@test.com' in data['data']['users'][2]['email']
|
assert 'sam@test.com' in data['data']['users'][2]['email']
|
||||||
|
assert data['data']['users'][0]['is_active']
|
||||||
|
assert not data['data']['users'][1]['is_active']
|
||||||
|
assert data['data']['users'][2]['is_active']
|
||||||
assert data['data']['users'][0]['imperial_units'] is False
|
assert data['data']['users'][0]['imperial_units'] is False
|
||||||
assert data['data']['users'][0]['timezone'] is None
|
assert data['data']['users'][0]['timezone'] is None
|
||||||
assert data['data']['users'][0]['weekm'] is False
|
assert data['data']['users'][0]['weekm'] is False
|
||||||
@ -223,6 +263,9 @@ class TestGetUsers(ApiTestCaseMixin):
|
|||||||
assert 'admin@example.com' in data['data']['users'][0]['email']
|
assert 'admin@example.com' in data['data']['users'][0]['email']
|
||||||
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
assert 'toto@toto.com' in data['data']['users'][1]['email']
|
||||||
assert 'sam@test.com' in data['data']['users'][2]['email']
|
assert 'sam@test.com' in data['data']['users'][2]['email']
|
||||||
|
assert data['data']['users'][0]['is_active']
|
||||||
|
assert data['data']['users'][1]['is_active']
|
||||||
|
assert data['data']['users'][2]['is_active']
|
||||||
assert data['data']['users'][0]['imperial_units'] is False
|
assert data['data']['users'][0]['imperial_units'] is False
|
||||||
assert data['data']['users'][0]['timezone'] is None
|
assert data['data']['users'][0]['timezone'] is None
|
||||||
assert data['data']['users'][0]['weekm'] is False
|
assert data['data']['users'][0]['weekm'] is False
|
||||||
@ -1344,7 +1387,7 @@ class TestDeleteUser(ApiTestCaseMixin):
|
|||||||
'you can not delete your account, no other user has admin rights',
|
'you can not delete your account, no other user has admin rights',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_it_enables_registration_on_user_delete(
|
def test_it_enables_registration_after_user_delete(
|
||||||
self,
|
self,
|
||||||
app_with_3_users_max: Flask,
|
app_with_3_users_max: Flask,
|
||||||
user_1_admin: User,
|
user_1_admin: User,
|
||||||
@ -1363,15 +1406,15 @@ class TestDeleteUser(ApiTestCaseMixin):
|
|||||||
'/api/auth/register',
|
'/api/auth/register',
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
dict(
|
dict(
|
||||||
username='justatest',
|
username=self.random_string(),
|
||||||
email='test@test.com',
|
email=self.random_email(),
|
||||||
password='12345678',
|
password=self.random_string(),
|
||||||
password_conf='12345678',
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
assert response.status_code == 201
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
def test_it_does_not_enable_registration_on_user_delete(
|
def test_it_does_not_enable_registration_on_user_delete(
|
||||||
self,
|
self,
|
||||||
|
@ -14,6 +14,7 @@ class TestUserModel:
|
|||||||
assert 'created_at' in serialized_user
|
assert 'created_at' in serialized_user
|
||||||
assert serialized_user['admin'] is False
|
assert serialized_user['admin'] is False
|
||||||
assert serialized_user['first_name'] is None
|
assert serialized_user['first_name'] is None
|
||||||
|
assert serialized_user['is_active']
|
||||||
assert serialized_user['last_name'] is None
|
assert serialized_user['last_name'] is None
|
||||||
assert serialized_user['bio'] is None
|
assert serialized_user['bio'] is None
|
||||||
assert serialized_user['location'] is None
|
assert serialized_user['location'] is None
|
||||||
@ -99,6 +100,15 @@ class TestUserModel:
|
|||||||
)
|
)
|
||||||
assert serialized_user['records'][0]['workout_date']
|
assert serialized_user['records'][0]['workout_date']
|
||||||
|
|
||||||
|
def test_it_returns_is_active_to_false_fot_inactive_user(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
inactive_user: User,
|
||||||
|
) -> None:
|
||||||
|
serialized_user = inactive_user.serialize(inactive_user)
|
||||||
|
|
||||||
|
assert serialized_user['is_active'] is False
|
||||||
|
|
||||||
|
|
||||||
class TestUserSportModel:
|
class TestUserSportModel:
|
||||||
def test_user_model(
|
def test_user_model(
|
||||||
|
@ -887,3 +887,13 @@ class TestGetRecords(ApiTestCaseMixin):
|
|||||||
assert workout_4_short_id == data['data']['records'][7]['workout_id']
|
assert workout_4_short_id == data['data']['records'][7]['workout_id']
|
||||||
assert 'MS' == data['data']['records'][7]['record_type']
|
assert 'MS' == data['data']['records'][7]['record_type']
|
||||||
assert 12.0 == data['data']['records'][7]['value']
|
assert 12.0 == data['data']['records'][7]['value']
|
||||||
|
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/api/records')
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
@ -45,6 +45,16 @@ expected_sport_1_cycling_inactive_admin_result['has_workouts'] = False
|
|||||||
|
|
||||||
|
|
||||||
class TestGetSports(ApiTestCaseMixin):
|
class TestGetSports(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/api/sports')
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_gets_all_sports(
|
def test_it_gets_all_sports(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
@ -9,6 +9,17 @@ from ..mixins import ApiTestCaseMixin
|
|||||||
|
|
||||||
|
|
||||||
class TestGetStatsByTime(ApiTestCaseMixin):
|
class TestGetStatsByTime(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f'/api/stats/{user_1.username}/by_time',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_gets_no_stats_when_user_has_no_workouts(
|
def test_it_gets_no_stats_when_user_has_no_workouts(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -853,6 +864,17 @@ class TestGetStatsByTime(ApiTestCaseMixin):
|
|||||||
|
|
||||||
|
|
||||||
class TestGetStatsBySport(ApiTestCaseMixin):
|
class TestGetStatsBySport(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f'/api/stats/{user_1.username}/by_sport',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_gets_stats_by_sport(
|
def test_it_gets_stats_by_sport(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
@ -987,6 +1009,15 @@ class TestGetStatsBySport(ApiTestCaseMixin):
|
|||||||
|
|
||||||
|
|
||||||
class TestGetAllStats(ApiTestCaseMixin):
|
class TestGetAllStats(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/api/stats/all')
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_returns_all_stats_when_users_have_no_workouts(
|
def test_it_returns_all_stats_when_users_have_no_workouts(
|
||||||
self, app: Flask, user_1_admin: User, user_2: User
|
self, app: Flask, user_1_admin: User, user_2: User
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -12,6 +12,15 @@ from .utils import get_random_short_id
|
|||||||
|
|
||||||
|
|
||||||
class TestGetWorkouts(ApiTestCaseMixin):
|
class TestGetWorkouts(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get('/api/workouts')
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_gets_all_workouts_for_authenticated_user(
|
def test_it_gets_all_workouts_for_authenticated_user(
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
|
@ -206,6 +206,22 @@ def assert_workout_data_wo_gpx(data: Dict) -> None:
|
|||||||
|
|
||||||
|
|
||||||
class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask, sport_1_cycling: Sport, gpx_file: str
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/workouts',
|
||||||
|
data=dict(
|
||||||
|
file=(BytesIO(str.encode(gpx_file)), 'example.gpx'),
|
||||||
|
data='{"sport_id": 1}',
|
||||||
|
),
|
||||||
|
headers=dict(content_type='multipart/form-data'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_adds_an_workout_with_gpx_file(
|
def test_it_adds_an_workout_with_gpx_file(
|
||||||
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
self, app: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -569,6 +585,27 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
|||||||
|
|
||||||
|
|
||||||
class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
class TestPostWorkoutWithoutGpx(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_error_if_user_is_not_authenticated(
|
||||||
|
self, app: Flask, sport_1_cycling: Sport, gpx_file: str
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/workouts/no_gpx',
|
||||||
|
content_type='application/json',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
sport_id=1,
|
||||||
|
duration=3600,
|
||||||
|
workout_date='2018-05-15 14:05',
|
||||||
|
distance=10,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
headers=dict(content_type='multipart/form-data'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
def test_it_adds_an_workout_without_gpx(
|
def test_it_adds_an_workout_without_gpx(
|
||||||
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
self, app: Flask, user_1: User, sport_1_cycling: Sport
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -6,12 +6,13 @@ from typing import Dict, Tuple, Union
|
|||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from flask import Blueprint, current_app, request
|
from flask import Blueprint, current_app, request
|
||||||
from sqlalchemy import exc, func, or_
|
from sqlalchemy import exc, func
|
||||||
from werkzeug.exceptions import RequestEntityTooLarge
|
from werkzeug.exceptions import RequestEntityTooLarge
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from fittrackee import appLog, bcrypt, db
|
from fittrackee import appLog, bcrypt, db
|
||||||
from fittrackee.emails.tasks import (
|
from fittrackee.emails.tasks import (
|
||||||
|
account_confirmation_email,
|
||||||
email_updated_to_current_address,
|
email_updated_to_current_address,
|
||||||
email_updated_to_new_address,
|
email_updated_to_new_address,
|
||||||
password_change_email,
|
password_change_email,
|
||||||
@ -46,6 +47,9 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
"""
|
"""
|
||||||
register a user
|
register a user
|
||||||
|
|
||||||
|
The newly created account is inactive. The user must confirm his email
|
||||||
|
to activate it.
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
@ -63,8 +67,6 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"auth_token": "JSON Web Token",
|
|
||||||
"message": "successfully registered",
|
|
||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,18 +78,18 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"message": "Errors: email: valid email must be provided\\n",
|
"message": "Errors: email: valid email must be provided\n",
|
||||||
"status": "error"
|
"status": "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
:<json string username: user name (3 to 30 characters required)
|
:<json string username: username (3 to 30 characters required)
|
||||||
:<json string email: user email
|
:<json string email: user email
|
||||||
:<json string password: password (8 characters required)
|
:<json string password: password (8 characters required)
|
||||||
|
|
||||||
:statuscode 201: successfully registered
|
:statuscode 201: successfully registered
|
||||||
:statuscode 400:
|
:statuscode 400:
|
||||||
- invalid payload
|
- invalid payload
|
||||||
- sorry, that user already exists
|
- sorry, that username is already taken
|
||||||
- Errors:
|
- Errors:
|
||||||
- username: 3 to 30 characters required
|
- username: 3 to 30 characters required
|
||||||
- username: only alphanumeric characters and the underscore
|
- username: only alphanumeric characters and the underscore
|
||||||
@ -125,30 +127,44 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
return InvalidPayloadErrorResponse(ret)
|
return InvalidPayloadErrorResponse(ret)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# check for existing user
|
|
||||||
user = User.query.filter(
|
user = User.query.filter(
|
||||||
or_(
|
func.lower(User.username) == func.lower(username)
|
||||||
func.lower(User.username) == func.lower(username),
|
|
||||||
func.lower(User.email) == func.lower(email),
|
|
||||||
)
|
|
||||||
).first()
|
).first()
|
||||||
if user:
|
if user:
|
||||||
return InvalidPayloadErrorResponse(
|
return InvalidPayloadErrorResponse(
|
||||||
'sorry, that user already exists'
|
'sorry, that username is already taken'
|
||||||
)
|
)
|
||||||
|
|
||||||
# add new user to db
|
# if a user exists with same email address, no error is returned
|
||||||
|
# since a user has to confirm his email to activate his account
|
||||||
|
user = User.query.filter(
|
||||||
|
func.lower(User.email) == func.lower(email)
|
||||||
|
).first()
|
||||||
|
if not user:
|
||||||
new_user = User(username=username, email=email, password=password)
|
new_user = User(username=username, email=email, password=password)
|
||||||
new_user.timezone = 'Europe/Paris'
|
new_user.timezone = 'Europe/Paris'
|
||||||
|
new_user.confirmation_token = secrets.token_urlsafe(16)
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
# generate auth token
|
|
||||||
auth_token = new_user.encode_auth_token(new_user.id)
|
ui_url = current_app.config['UI_URL']
|
||||||
return {
|
email_data = {
|
||||||
'status': 'success',
|
'username': new_user.username,
|
||||||
'message': 'successfully registered',
|
'fittrackee_url': ui_url,
|
||||||
'auth_token': auth_token,
|
'operating_system': request.user_agent.platform, # type: ignore # noqa
|
||||||
}, 201
|
'browser_name': request.user_agent.browser, # type: ignore
|
||||||
|
'account_confirmation_url': (
|
||||||
|
f'{ui_url}/account-confirmation'
|
||||||
|
f'?token={new_user.confirmation_token}'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
user_data = {
|
||||||
|
'language': 'en',
|
||||||
|
'email': new_user.email,
|
||||||
|
}
|
||||||
|
account_confirmation_email.send(user_data, email_data)
|
||||||
|
|
||||||
|
return {'status': 'success'}, 200
|
||||||
# handler errors
|
# handler errors
|
||||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
return handle_error_and_return_response(e, db=db)
|
return handle_error_and_return_response(e, db=db)
|
||||||
@ -158,6 +174,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
|
|||||||
def login_user() -> Union[Dict, HttpResponse]:
|
def login_user() -> Union[Dict, HttpResponse]:
|
||||||
"""
|
"""
|
||||||
user login
|
user login
|
||||||
|
Only user with active account can log in
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -209,9 +226,9 @@ def login_user() -> Union[Dict, HttpResponse]:
|
|||||||
email = post_data.get('email', '')
|
email = post_data.get('email', '')
|
||||||
password = post_data.get('password')
|
password = post_data.get('password')
|
||||||
try:
|
try:
|
||||||
# check for existing user
|
|
||||||
user = User.query.filter(
|
user = User.query.filter(
|
||||||
func.lower(User.email) == func.lower(email)
|
func.lower(User.email) == func.lower(email),
|
||||||
|
User.is_active == True, # noqa
|
||||||
).first()
|
).first()
|
||||||
if user and bcrypt.check_password_hash(user.password, password):
|
if user and bcrypt.check_password_hash(user.password, password):
|
||||||
# generate auth token
|
# generate auth token
|
||||||
@ -258,6 +275,7 @@ def get_authenticated_user_profile(
|
|||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_active": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -357,6 +375,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_active": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -516,6 +535,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_active": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -709,6 +729,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_active": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -1312,3 +1333,63 @@ def update_email() -> Union[Dict, HttpResponse]:
|
|||||||
|
|
||||||
except (exc.OperationalError, ValueError) as e:
|
except (exc.OperationalError, ValueError) as e:
|
||||||
return handle_error_and_return_response(e, db=db)
|
return handle_error_and_return_response(e, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/account/confirm', methods=['POST'])
|
||||||
|
def confirm_account() -> Union[Dict, HttpResponse]:
|
||||||
|
"""
|
||||||
|
activate user account after registration
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/auth/account/confirm HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"auth_token": "JSON Web Token",
|
||||||
|
"message": "account confirmation successful",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
:<json string token: confirmation token
|
||||||
|
|
||||||
|
:statuscode 200: account confirmation successful
|
||||||
|
:statuscode 400: invalid payload
|
||||||
|
:statuscode 500: error, please try again or contact the administrator
|
||||||
|
|
||||||
|
"""
|
||||||
|
post_data = request.get_json()
|
||||||
|
if not post_data or post_data.get('token') is None:
|
||||||
|
return InvalidPayloadErrorResponse()
|
||||||
|
token = post_data.get('token')
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.query.filter_by(confirmation_token=token).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return InvalidPayloadErrorResponse()
|
||||||
|
|
||||||
|
user.is_active = True
|
||||||
|
user.confirmation_token = None
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# generate auth token
|
||||||
|
auth_token = user.encode_auth_token(user.id)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'account confirmation successful',
|
||||||
|
'auth_token': auth_token,
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
|
||||||
|
except (exc.OperationalError, ValueError) as e:
|
||||||
|
return handle_error_and_return_response(e, db=db)
|
||||||
|
@ -47,6 +47,7 @@ class User(BaseModel):
|
|||||||
)
|
)
|
||||||
language = db.Column(db.String(50), nullable=True)
|
language = db.Column(db.String(50), nullable=True)
|
||||||
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
imperial_units = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
|
is_active = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
email_to_confirm = db.Column(db.String(255), nullable=True)
|
email_to_confirm = db.Column(db.String(255), nullable=True)
|
||||||
confirmation_token = db.Column(db.String(255), nullable=True)
|
confirmation_token = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ class User(BaseModel):
|
|||||||
'email': self.email,
|
'email': self.email,
|
||||||
'email_to_confirm': self.email_to_confirm,
|
'email_to_confirm': self.email_to_confirm,
|
||||||
'first_name': self.first_name,
|
'first_name': self.first_name,
|
||||||
|
'is_active': self.is_active,
|
||||||
'last_name': self.last_name,
|
'last_name': self.last_name,
|
||||||
'location': self.location,
|
'location': self.location,
|
||||||
'nb_sports': len(sports),
|
'nb_sports': len(sports),
|
||||||
|
@ -52,7 +52,7 @@ def set_admin(username: str) -> None:
|
|||||||
@authenticate_as_admin
|
@authenticate_as_admin
|
||||||
def get_users(auth_user: User) -> Dict:
|
def get_users(auth_user: User) -> Dict:
|
||||||
"""
|
"""
|
||||||
Get all users
|
Get all users (regardless their account status)
|
||||||
|
|
||||||
**Example request**:
|
**Example request**:
|
||||||
|
|
||||||
@ -87,6 +87,7 @@ def get_users(auth_user: User) -> Dict:
|
|||||||
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
"created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
|
||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"is_admin": true,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
@ -149,6 +150,7 @@ def get_users(auth_user: User) -> Dict:
|
|||||||
"created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
|
"created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
|
||||||
"email": "sam@example.com",
|
"email": "sam@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
|
"is_admin": false,
|
||||||
"language": "fr",
|
"language": "fr",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -269,6 +271,7 @@ def get_single_user(
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_admin": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
@ -421,6 +424,7 @@ def update_user(auth_user: User, user_name: str) -> Union[Dict, HttpResponse]:
|
|||||||
"email": "admin@example.com",
|
"email": "admin@example.com",
|
||||||
"first_name": null,
|
"first_name": null,
|
||||||
"imperial_units": false,
|
"imperial_units": false,
|
||||||
|
"is_active": true,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"last_name": null,
|
"last_name": null,
|
||||||
"location": null,
|
"location": null,
|
||||||
|
@ -62,8 +62,12 @@ def verify_user(
|
|||||||
current_request: Request, verify_admin: bool
|
current_request: Request, verify_admin: bool
|
||||||
) -> Tuple[Optional[HttpResponse], Optional[User]]:
|
) -> Tuple[Optional[HttpResponse], Optional[User]]:
|
||||||
"""
|
"""
|
||||||
Return authenticated user, if the provided token is valid and user has
|
Return authenticated user if
|
||||||
admin rights if 'verify_admin' is True
|
- the provided token is valid
|
||||||
|
- the user account is active
|
||||||
|
- the user has admin rights if 'verify_admin' is True
|
||||||
|
|
||||||
|
If not, it returns Error Response
|
||||||
"""
|
"""
|
||||||
default_message = 'provide a valid auth token'
|
default_message = 'provide a valid auth token'
|
||||||
auth_header = current_request.headers.get('Authorization')
|
auth_header = current_request.headers.get('Authorization')
|
||||||
@ -74,7 +78,7 @@ def verify_user(
|
|||||||
if isinstance(resp, str):
|
if isinstance(resp, str):
|
||||||
return UnauthorizedErrorResponse(resp), None
|
return UnauthorizedErrorResponse(resp), None
|
||||||
user = User.query.filter_by(id=resp).first()
|
user = User.query.filter_by(id=resp).first()
|
||||||
if not user:
|
if not user or not user.is_active:
|
||||||
return UnauthorizedErrorResponse(default_message), None
|
return UnauthorizedErrorResponse(default_message), None
|
||||||
if verify_admin and not user.admin:
|
if verify_admin and not user.admin:
|
||||||
return ForbiddenErrorResponse(), None
|
return ForbiddenErrorResponse(), None
|
||||||
|
Loading…
Reference in New Issue
Block a user