API - add route to update password and email templates - #50
This commit is contained in:
parent
d6ce02d385
commit
51d627fa1c
2
.flake8
2
.flake8
@ -1,3 +1,5 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
per-file-ignores =
|
per-file-ignores =
|
||||||
fittrackee_api/fittrackee_api/activities/stats.py:E501
|
fittrackee_api/fittrackee_api/activities/stats.py:E501
|
||||||
|
fittrackee_api/fittrackee_api/tests/test_email.py:E501
|
||||||
|
fittrackee_api/fittrackee_api/tests/test_email_template_password_request.py:E501
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
export REACT_APP_API_URL = http://$(HOST):$(API_PORT)
|
export REACT_APP_API_URL=
|
||||||
export REACT_APP_THUNDERFOREST_API_KEY=
|
export REACT_APP_THUNDERFOREST_API_KEY=
|
||||||
export WEATHER_API=
|
export WEATHER_API=
|
||||||
|
export UI_URL=
|
||||||
|
export EMAIL_URL=
|
||||||
|
export SENDER_EMAIL=
|
||||||
|
|
||||||
# for dev env
|
# for dev env
|
||||||
export CODACY_PROJECT_TOKEN=
|
export CODACY_PROJECT_TOKEN=
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from importlib import import_module, reload
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_bcrypt import Bcrypt
|
from flask_bcrypt import Bcrypt
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
from .email.email import Email
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
|
email_service = Email()
|
||||||
appLog = logging.getLogger('fittrackee_api')
|
appLog = logging.getLogger('fittrackee_api')
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +23,10 @@ def create_app():
|
|||||||
# set config
|
# set config
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
app_settings = os.getenv('APP_SETTINGS')
|
app_settings = os.getenv('APP_SETTINGS')
|
||||||
|
if app_settings == 'fittrackee_api.config.TestingConfig':
|
||||||
|
# reload config on tests
|
||||||
|
config = import_module('fittrackee_api.config')
|
||||||
|
reload(config)
|
||||||
app.config.from_object(app_settings)
|
app.config.from_object(app_settings)
|
||||||
|
|
||||||
# set up extensions
|
# set up extensions
|
||||||
@ -26,6 +34,9 @@ def create_app():
|
|||||||
bcrypt.init_app(app)
|
bcrypt.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
|
# set up email
|
||||||
|
email_service.init_email(app)
|
||||||
|
|
||||||
# get configuration from database
|
# get configuration from database
|
||||||
from .application.models import AppConfig
|
from .application.models import AppConfig
|
||||||
from .application.utils import init_config, update_app_config_from_database
|
from .application.utils import init_config, update_app_config_from_database
|
||||||
@ -64,7 +75,6 @@ def create_app():
|
|||||||
logging.getLogger('flake8').propagate = False
|
logging.getLogger('flake8').propagate = False
|
||||||
appLog.setLevel(logging.DEBUG)
|
appLog.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
if app.debug:
|
|
||||||
# Enable CORS
|
# Enable CORS
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def after_request(response):
|
def after_request(response):
|
||||||
|
@ -16,6 +16,10 @@ class BaseConfig:
|
|||||||
UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads')
|
UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads')
|
||||||
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
|
PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
|
||||||
ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'}
|
ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'}
|
||||||
|
TEMPLATES_FOLDER = os.path.join(current_app.root_path, 'email/templates')
|
||||||
|
UI_URL = os.environ.get('UI_URL')
|
||||||
|
EMAIL_URL = os.environ.get('EMAIL_URL')
|
||||||
|
SENDER_EMAIL = os.environ.get('SENDER_EMAIL')
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentConfig(BaseConfig):
|
class DevelopmentConfig(BaseConfig):
|
||||||
@ -41,4 +45,5 @@ class TestingConfig(BaseConfig):
|
|||||||
BCRYPT_LOG_ROUNDS = 4
|
BCRYPT_LOG_ROUNDS = 4
|
||||||
TOKEN_EXPIRATION_DAYS = 0
|
TOKEN_EXPIRATION_DAYS = 0
|
||||||
TOKEN_EXPIRATION_SECONDS = 3
|
TOKEN_EXPIRATION_SECONDS = 3
|
||||||
|
PASSWORD_TOKEN_EXPIRATION_SECONDS = 3
|
||||||
UPLOAD_FOLDER = '/tmp/fitTrackee/uploads'
|
UPLOAD_FOLDER = '/tmp/fitTrackee/uploads'
|
||||||
|
103
fittrackee_api/fittrackee_api/email/email.py
Normal file
103
fittrackee_api/fittrackee_api/email/email.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import logging
|
||||||
|
import smtplib
|
||||||
|
import ssl
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
from .utils_email import parse_email_url
|
||||||
|
|
||||||
|
email_log = logging.getLogger('fittrackee_api_email')
|
||||||
|
email_log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailMessage:
|
||||||
|
def __init__(self, sender, recipient, subject, html, text):
|
||||||
|
self.sender = sender
|
||||||
|
self.recipient = recipient
|
||||||
|
self.subject = subject
|
||||||
|
self.html = html
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def generate_message(self):
|
||||||
|
message = MIMEMultipart('alternative')
|
||||||
|
message['Subject'] = self.subject
|
||||||
|
message['From'] = self.sender
|
||||||
|
message['To'] = self.recipient
|
||||||
|
part1 = MIMEText(self.text, 'plain')
|
||||||
|
part2 = MIMEText(self.html, 'html')
|
||||||
|
message.attach(part1)
|
||||||
|
message.attach(part2)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
class EmailTemplate:
|
||||||
|
def __init__(self, template_directory):
|
||||||
|
self._env = Environment(loader=FileSystemLoader(template_directory))
|
||||||
|
|
||||||
|
def get_content(self, template, lang, part, data):
|
||||||
|
template = self._env.get_template(f'{template}/{lang}/{part}')
|
||||||
|
return template.render(data)
|
||||||
|
|
||||||
|
def get_all_contents(self, template, lang, data):
|
||||||
|
output = {}
|
||||||
|
for part in ['subject.txt', 'body.txt', 'body.html']:
|
||||||
|
output[part] = self.get_content(template, lang, part, data)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_message(self, template, lang, sender, recipient, data):
|
||||||
|
output = self.get_all_contents(template, lang, data)
|
||||||
|
message = EmailMessage(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
output['subject.txt'],
|
||||||
|
output['body.html'],
|
||||||
|
output['body.txt'],
|
||||||
|
)
|
||||||
|
return message.generate_message()
|
||||||
|
|
||||||
|
|
||||||
|
class Email:
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.host = 'localhost'
|
||||||
|
self.port = 1025
|
||||||
|
self.use_tls = False
|
||||||
|
self.use_ssl = False
|
||||||
|
self.username = None
|
||||||
|
self.password = None
|
||||||
|
self.sender_email = 'no-reply@example.com'
|
||||||
|
self.email_template = None
|
||||||
|
if app is not None:
|
||||||
|
self.init_email(app)
|
||||||
|
|
||||||
|
def init_email(self, app):
|
||||||
|
parsed_url = parse_email_url(app.config.get('EMAIL_URL'))
|
||||||
|
self.host = parsed_url['host']
|
||||||
|
self.port = parsed_url['port']
|
||||||
|
self.use_tls = parsed_url['use_tls']
|
||||||
|
self.use_ssl = parsed_url['use_ssl']
|
||||||
|
self.username = parsed_url['username']
|
||||||
|
self.password = parsed_url['password']
|
||||||
|
self.sender_email = app.config.get('SENDER_EMAIL')
|
||||||
|
self.email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def smtp(self):
|
||||||
|
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
|
||||||
|
|
||||||
|
def send(self, template, lang, recipient, data):
|
||||||
|
message = self.email_template.get_message(
|
||||||
|
template, lang, self.sender_email, recipient, data
|
||||||
|
)
|
||||||
|
connection_params = {}
|
||||||
|
if self.use_ssl or self.use_tls:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
if self.use_ssl:
|
||||||
|
connection_params.update({'context': context})
|
||||||
|
with self.smtp(self.host, self.port, **connection_params) as smtp:
|
||||||
|
smtp.login(self.username, self.password)
|
||||||
|
if self.use_tls:
|
||||||
|
smtp.starttls(context=context)
|
||||||
|
smtp.sendmail(self.sender_email, recipient, message.as_string())
|
||||||
|
smtp.quit()
|
@ -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 - Password reset request</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 reset your password. The link is only valid for 24 hours.</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="https://example.com" 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 recently requested to reset your password for your account. Use the button below to reset it.
|
||||||
|
<strong>This password reset is only valid for the next 24 hours.</strong>
|
||||||
|
</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="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
For security, this request was received from a {{operating_system}} device using {{browser_name}}.
|
||||||
|
If you did not request a password reset, 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">{{password_reset_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>
|
@ -0,0 +1,10 @@
|
|||||||
|
Hi {{username}},
|
||||||
|
|
||||||
|
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset is only valid for the next 24 hours.
|
||||||
|
|
||||||
|
Reset your password ( {{ password_reset_url }} )
|
||||||
|
|
||||||
|
For security, this request was received from a {{operating_system}} device using {{browser_name}}. If you did not request a password reset, please ignore this email.
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The FitTrackee Team
|
@ -0,0 +1 @@
|
|||||||
|
FitTrackee - Password reset request
|
@ -0,0 +1,270 @@
|
|||||||
|
<!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 - Réinitialiser le mot de passe</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 reset your password. The link is only valid for 24 hours.</span>
|
||||||
|
<span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 1 heure.</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="https://example.com" 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 récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||||
|
Cliquez sur le bouton ci-dessous pour le réinitialiser.
|
||||||
|
<strong>Cette réinitialisation n'est valide que pendant 1 heure.</strong>
|
||||||
|
</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="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Réinitialiser le mot de passe</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||||
|
Si vous n'avez pas demandé de réinitalisation, 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">{{password_reset_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>
|
@ -0,0 +1,12 @@
|
|||||||
|
Bonjour {{username}},
|
||||||
|
|
||||||
|
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 1 heure.
|
||||||
|
|
||||||
|
Réinitialiser le mot de passe: ( {{ password_reset_url }} )
|
||||||
|
|
||||||
|
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
|
||||||
|
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||||
|
|
||||||
|
Merci,
|
||||||
|
L'équipe FitTrackee
|
@ -0,0 +1 @@
|
|||||||
|
FitTrackee - Réinitialiser votre mot de passe
|
20
fittrackee_api/fittrackee_api/email/utils_email.py
Normal file
20
fittrackee_api/fittrackee_api/email/utils_email.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from urllib3.util import parse_url
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidEmailUrlScheme(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def parse_email_url(email_url):
|
||||||
|
parsed_url = parse_url(email_url)
|
||||||
|
if parsed_url.scheme != 'smtp':
|
||||||
|
raise InvalidEmailUrlScheme()
|
||||||
|
credentials = parsed_url.auth.split(':')
|
||||||
|
return {
|
||||||
|
'host': parsed_url.host,
|
||||||
|
'port': parsed_url.port,
|
||||||
|
'use_tls': True if parsed_url.query == 'tls=True' else False,
|
||||||
|
'use_ssl': True if parsed_url.query == 'ssl=True' else False,
|
||||||
|
'username': credentials[0],
|
||||||
|
'password': credentials[1],
|
||||||
|
}
|
@ -46,7 +46,8 @@ def get_app(with_config=False):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app():
|
def app(monkeypatch):
|
||||||
|
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025')
|
||||||
yield from get_app(with_config=True)
|
yield from get_app(with_config=True)
|
||||||
|
|
||||||
|
|
||||||
@ -55,6 +56,20 @@ def app_no_config():
|
|||||||
yield from get_app(with_config=False)
|
yield from get_app(with_config=False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_ssl(monkeypatch):
|
||||||
|
print('app')
|
||||||
|
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?ssl=True')
|
||||||
|
yield from get_app(with_config=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_tls(monkeypatch):
|
||||||
|
print('app')
|
||||||
|
monkeypatch.setenv('EMAIL_URL', 'smtp://none:none@0.0.0.0:1025?tls=True')
|
||||||
|
yield from get_app(with_config=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def app_config():
|
def app_config():
|
||||||
config = AppConfig()
|
config = AppConfig()
|
||||||
|
@ -0,0 +1,175 @@
|
|||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
expected_en_text_body = """Hi test,
|
||||||
|
|
||||||
|
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset is only valid for the next 24 hours.
|
||||||
|
|
||||||
|
Reset your password ( http://localhost/password-reset?token=xxx )
|
||||||
|
|
||||||
|
For security, this request was received from a Linux device using Firefox. If you did not request a password reset, please ignore this email.
|
||||||
|
|
||||||
|
Thanks,
|
||||||
|
The FitTrackee Team"""
|
||||||
|
|
||||||
|
expected_fr_text_body = """Bonjour test,
|
||||||
|
|
||||||
|
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||||
|
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant 1 heure.
|
||||||
|
|
||||||
|
Réinitialiser le mot de passe: ( http://localhost/password-reset?token=xxx )
|
||||||
|
|
||||||
|
Pour vérification, cette demande a été reçue à partir d'un appareil sous Linux, utilisant le navigateur Firefox.
|
||||||
|
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
|
||||||
|
|
||||||
|
Merci,
|
||||||
|
L'équipe FitTrackee"""
|
||||||
|
|
||||||
|
expected_en_html_body = """ <body>
|
||||||
|
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</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="https://example.com" 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 recently requested to reset your password for your account. Use the button below to reset it.
|
||||||
|
<strong>This password reset is only valid for the next 24 hours.</strong>
|
||||||
|
</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/password-reset?token=xxx" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
For security, this request was received from a Linux device using Firefox.
|
||||||
|
If you did not request a password reset, 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/password-reset?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">Use this link to reset your password. The link is only valid for 24 hours.</span>
|
||||||
|
<span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant 1 heure.</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="https://example.com" 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 récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
|
||||||
|
Cliquez sur le bouton ci-dessous pour le réinitialiser.
|
||||||
|
<strong>Cette réinitialisation n'est valide que pendant 1 heure.</strong>
|
||||||
|
</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/password-reset?token=xxx" class="f-fallback button button--green" target="_blank">Réinitialiser le mot de passe</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'avez pas demandé de réinitalisation, 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/password-reset?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>"""
|
@ -1,6 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
from datetime import datetime, timedelta
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from fittrackee_api.users.utils_token import get_user_token
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
|
||||||
class TestUserRegistration:
|
class TestUserRegistration:
|
||||||
@ -352,13 +356,13 @@ class TestUserLogout:
|
|||||||
|
|
||||||
def test_it_returns_error_with_expired_token(self, app, user_1):
|
def test_it_returns_error_with_expired_token(self, app, user_1):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
|
now = datetime.utcnow()
|
||||||
resp_login = client.post(
|
resp_login = client.post(
|
||||||
'/api/auth/login',
|
'/api/auth/login',
|
||||||
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
data=json.dumps(dict(email='test@test.com', password='12345678')),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
# invalid token logout
|
with freeze_time(now + timedelta(seconds=4)):
|
||||||
time.sleep(4)
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
'/api/auth/logout',
|
'/api/auth/logout',
|
||||||
headers=dict(
|
headers=dict(
|
||||||
@ -915,10 +919,14 @@ class TestRegistrationConfiguration:
|
|||||||
|
|
||||||
|
|
||||||
class TestPasswordResetRequest:
|
class TestPasswordResetRequest:
|
||||||
def test_it_requests_password_reset_when_user_exists(self, app, user_1):
|
@patch('smtplib.SMTP_SSL')
|
||||||
|
@patch('smtplib.SMTP')
|
||||||
|
def test_it_requests_password_reset_when_user_exists(
|
||||||
|
self, mock_smtp, mock_smtp_ssl, app, user_1
|
||||||
|
):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/api/auth/password-reset/request',
|
'/api/auth/password/reset-request',
|
||||||
data=json.dumps(dict(email='test@test.com')),
|
data=json.dumps(dict(email='test@test.com')),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
@ -932,7 +940,7 @@ class TestPasswordResetRequest:
|
|||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/api/auth/password-reset/request',
|
'/api/auth/password/reset-request',
|
||||||
data=json.dumps(dict(email='test@test.com')),
|
data=json.dumps(dict(email='test@test.com')),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
@ -946,7 +954,7 @@ class TestPasswordResetRequest:
|
|||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/api/auth/password-reset/request',
|
'/api/auth/password/reset-request',
|
||||||
data=json.dumps(dict(usernmae='test')),
|
data=json.dumps(dict(usernmae='test')),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
@ -960,7 +968,7 @@ class TestPasswordResetRequest:
|
|||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/api/auth/password-reset/request',
|
'/api/auth/password/reset-request',
|
||||||
data=json.dumps(dict()),
|
data=json.dumps(dict()),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
@ -969,3 +977,151 @@ class TestPasswordResetRequest:
|
|||||||
data = json.loads(response.data.decode())
|
data = json.loads(response.data.decode())
|
||||||
assert data['message'] == 'Invalid payload.'
|
assert data['message'] == 'Invalid payload.'
|
||||||
assert data['status'] == 'error'
|
assert data['status'] == 'error'
|
||||||
|
|
||||||
|
|
||||||
|
class TestPasswordUpdate:
|
||||||
|
def test_it_returns_error_if_payload_is_empty(self, app):
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(dict(token='xxx', password='1234567',)),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid payload.'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_token_is_missing(self, app):
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(password='12345678', password_conf='12345678',)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid payload.'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_password_is_missing(self, app):
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(dict(token='xxx', password_conf='12345678',)),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid payload.'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_password_confirmation_is_missing(self, app):
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(dict(token='xxx', password='12345678',)),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid payload.'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_token_is_invalid(self, app):
|
||||||
|
token = get_user_token(1)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
token=token.decode(),
|
||||||
|
password='12345678',
|
||||||
|
password_conf='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Invalid token. Please request a new token.'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_token_is_expired(self, app, user_1):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
token = get_user_token(user_1.id, password_reset=True)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
with freeze_time(now + timedelta(seconds=4)):
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
token=token.decode(),
|
||||||
|
password='12345678',
|
||||||
|
password_conf='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert (
|
||||||
|
data['message'] == 'Invalid token. Please request a new token.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_returns_error_if_password_is_invalid(self, app, user_1):
|
||||||
|
token = get_user_token(user_1.id, password_reset=True)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
token=token.decode(),
|
||||||
|
password='1234567',
|
||||||
|
password_conf='1234567',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'error'
|
||||||
|
assert data['message'] == 'Password: 8 characters required.\n'
|
||||||
|
|
||||||
|
def test_it_update_password(self, app, user_1):
|
||||||
|
token = get_user_token(user_1.id, password_reset=True)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/password/update',
|
||||||
|
data=json.dumps(
|
||||||
|
dict(
|
||||||
|
token=token.decode(),
|
||||||
|
password='12345678',
|
||||||
|
password_conf='12345678',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['message'] == 'Password updated.'
|
||||||
|
94
fittrackee_api/fittrackee_api/tests/test_email.py
Normal file
94
fittrackee_api/fittrackee_api/tests/test_email.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from fittrackee_api import email_service
|
||||||
|
from fittrackee_api.email.email import EmailMessage
|
||||||
|
|
||||||
|
from .template_results.password_reset_request import expected_en_text_body
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailMessage:
|
||||||
|
def test_it_generate_email_data(self):
|
||||||
|
message = EmailMessage(
|
||||||
|
sender='fittrackee@example.com',
|
||||||
|
recipient='test@test.com',
|
||||||
|
subject='Fittrackee - test email',
|
||||||
|
html="""\
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<p>Hello !</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""",
|
||||||
|
text='Hello !',
|
||||||
|
)
|
||||||
|
message_data = message.generate_message()
|
||||||
|
assert message_data.get('From') == 'fittrackee@example.com'
|
||||||
|
assert message_data.get('To') == 'test@test.com'
|
||||||
|
assert message_data.get('Subject') == 'Fittrackee - test email'
|
||||||
|
message_string = message_data.as_string()
|
||||||
|
assert 'Hello !' in message_string
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailSending:
|
||||||
|
|
||||||
|
email_data = {
|
||||||
|
'username': 'test',
|
||||||
|
'password_reset_url': f'http://localhost/password-reset?token=xxx',
|
||||||
|
'operating_system': 'Linux',
|
||||||
|
'browser_name': 'Firefox',
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_smtp(smtp):
|
||||||
|
assert smtp.sendmail.call_count == 1
|
||||||
|
call_args = smtp.sendmail.call_args.args
|
||||||
|
assert call_args[0] == 'fittrackee@example.com'
|
||||||
|
assert call_args[1] == 'test@test.com'
|
||||||
|
assert expected_en_text_body in call_args[2]
|
||||||
|
|
||||||
|
@patch('smtplib.SMTP_SSL')
|
||||||
|
@patch('smtplib.SMTP')
|
||||||
|
def test_it_sends_message(self, mock_smtp, mock_smtp_ssl, app):
|
||||||
|
|
||||||
|
email_service.send(
|
||||||
|
template='password_reset_request',
|
||||||
|
lang='en',
|
||||||
|
recipient='test@test.com',
|
||||||
|
data=self.email_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
smtp = mock_smtp.return_value.__enter__.return_value
|
||||||
|
assert smtp.starttls.not_called
|
||||||
|
self.assert_smtp(smtp)
|
||||||
|
|
||||||
|
@patch('smtplib.SMTP_SSL')
|
||||||
|
@patch('smtplib.SMTP')
|
||||||
|
def test_it_sends_message_with_ssl(
|
||||||
|
self, mock_smtp, mock_smtp_ssl, app_ssl
|
||||||
|
):
|
||||||
|
email_service.send(
|
||||||
|
template='password_reset_request',
|
||||||
|
lang='en',
|
||||||
|
recipient='test@test.com',
|
||||||
|
data=self.email_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
smtp = mock_smtp_ssl.return_value.__enter__.return_value
|
||||||
|
assert smtp.starttls.not_called
|
||||||
|
self.assert_smtp(smtp)
|
||||||
|
|
||||||
|
@patch('smtplib.SMTP_SSL')
|
||||||
|
@patch('smtplib.SMTP')
|
||||||
|
def test_it_sends_message_with_tls(
|
||||||
|
self, mock_smtp, mock_smtp_ssl, app_tls
|
||||||
|
):
|
||||||
|
email_service.send(
|
||||||
|
template='password_reset_request',
|
||||||
|
lang='en',
|
||||||
|
recipient='test@test.com',
|
||||||
|
data=self.email_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
smtp = mock_smtp.return_value.__enter__.return_value
|
||||||
|
assert smtp.starttls.call_count == 1
|
||||||
|
self.assert_smtp(smtp)
|
@ -0,0 +1,76 @@
|
|||||||
|
import pytest
|
||||||
|
from fittrackee_api.email.email import EmailTemplate
|
||||||
|
|
||||||
|
from .template_results.password_reset_request import (
|
||||||
|
expected_en_html_body,
|
||||||
|
expected_en_text_body,
|
||||||
|
expected_fr_html_body,
|
||||||
|
expected_fr_text_body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailTemplateForPasswordRequest:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'lang, expected_subject',
|
||||||
|
[
|
||||||
|
('en', 'FitTrackee - Password reset request'),
|
||||||
|
('fr', 'FitTrackee - Réinitialiser votre mot de passe'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_it_gets_subject(self, app, lang, expected_subject):
|
||||||
|
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||||
|
|
||||||
|
subject = email_template.get_content(
|
||||||
|
'password_reset_request', 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, lang, expected_text_body):
|
||||||
|
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||||
|
email_data = {
|
||||||
|
'username': 'test',
|
||||||
|
'password_reset_url': f'http://localhost/password-reset?token=xxx',
|
||||||
|
'operating_system': 'Linux',
|
||||||
|
'browser_name': 'Firefox',
|
||||||
|
}
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'password_reset_request', lang, 'body.txt', email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert text_body == expected_text_body
|
||||||
|
|
||||||
|
def test_it_gets_en_html_body(self, app):
|
||||||
|
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||||
|
email_data = {
|
||||||
|
'username': 'test',
|
||||||
|
'password_reset_url': f'http://localhost/password-reset?token=xxx',
|
||||||
|
'operating_system': 'Linux',
|
||||||
|
'browser_name': 'Firefox',
|
||||||
|
}
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'password_reset_request', 'en', 'body.html', email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_en_html_body in text_body
|
||||||
|
|
||||||
|
def test_it_gets_fr_html_body(self, app):
|
||||||
|
email_template = EmailTemplate(app.config.get('TEMPLATES_FOLDER'))
|
||||||
|
email_data = {
|
||||||
|
'username': 'test',
|
||||||
|
'password_reset_url': f'http://localhost/password-reset?token=xxx',
|
||||||
|
'operating_system': 'Linux',
|
||||||
|
'browser_name': 'Firefox',
|
||||||
|
}
|
||||||
|
|
||||||
|
text_body = email_template.get_content(
|
||||||
|
'password_reset_request', 'fr', 'body.html', email_data
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_fr_html_body in text_body
|
42
fittrackee_api/fittrackee_api/tests/test_email_utils.py
Normal file
42
fittrackee_api/fittrackee_api/tests/test_email_utils.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
from fittrackee_api.email.utils_email import (
|
||||||
|
InvalidEmailUrlScheme,
|
||||||
|
parse_email_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailUrlParser:
|
||||||
|
def test_it_raises_error_if_url_scheme_is_invalid(self):
|
||||||
|
url = 'stmp://username:password@localhost:587'
|
||||||
|
with pytest.raises(InvalidEmailUrlScheme):
|
||||||
|
parse_email_url(url)
|
||||||
|
|
||||||
|
def test_it_parses_email_url(self):
|
||||||
|
url = 'smtp://test@example.com:12345678@localhost:25'
|
||||||
|
parsed_email = parse_email_url(url)
|
||||||
|
assert parsed_email['username'] == 'test@example.com'
|
||||||
|
assert parsed_email['password'] == '12345678'
|
||||||
|
assert parsed_email['host'] == 'localhost'
|
||||||
|
assert parsed_email['port'] == 25
|
||||||
|
assert parsed_email['use_tls'] is False
|
||||||
|
assert parsed_email['use_ssl'] is False
|
||||||
|
|
||||||
|
def test_it_parses_email_url_with_tls(self):
|
||||||
|
url = 'smtp://test@example.com:12345678@localhost:587?tls=True'
|
||||||
|
parsed_email = parse_email_url(url)
|
||||||
|
assert parsed_email['username'] == 'test@example.com'
|
||||||
|
assert parsed_email['password'] == '12345678'
|
||||||
|
assert parsed_email['host'] == 'localhost'
|
||||||
|
assert parsed_email['port'] == 587
|
||||||
|
assert parsed_email['use_tls'] is True
|
||||||
|
assert parsed_email['use_ssl'] is False
|
||||||
|
|
||||||
|
def test_it_parses_email_url_with_ssl(self):
|
||||||
|
url = 'smtp://test@example.com:12345678@localhost:465?ssl=True'
|
||||||
|
parsed_email = parse_email_url(url)
|
||||||
|
assert parsed_email['username'] == 'test@example.com'
|
||||||
|
assert parsed_email['password'] == '12345678'
|
||||||
|
assert parsed_email['host'] == 'localhost'
|
||||||
|
assert parsed_email['port'] == 465
|
||||||
|
assert parsed_email['use_tls'] is False
|
||||||
|
assert parsed_email['use_ssl'] is True
|
@ -27,6 +27,10 @@ class TestUserModel:
|
|||||||
auth_token = user_1.encode_auth_token(user_1.id)
|
auth_token = user_1.encode_auth_token(user_1.id)
|
||||||
assert isinstance(auth_token, bytes)
|
assert isinstance(auth_token, bytes)
|
||||||
|
|
||||||
|
def test_encode_password_token(self, app, user_1):
|
||||||
|
password_token = user_1.encode_password_reset_token(user_1.id)
|
||||||
|
assert isinstance(password_token, bytes)
|
||||||
|
|
||||||
def test_decode_auth_token(self, app, user_1):
|
def test_decode_auth_token(self, app, user_1):
|
||||||
auth_token = user_1.encode_auth_token(user_1.id)
|
auth_token = user_1.encode_auth_token(user_1.id)
|
||||||
assert isinstance(auth_token, bytes)
|
assert isinstance(auth_token, bytes)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fittrackee_api import appLog, bcrypt, db
|
import jwt
|
||||||
|
from fittrackee_api import appLog, bcrypt, db, email_service
|
||||||
from flask import Blueprint, current_app, jsonify, request
|
from flask import Blueprint, current_app, jsonify, request
|
||||||
from sqlalchemy import exc, or_
|
from sqlalchemy import exc, or_
|
||||||
from werkzeug.exceptions import RequestEntityTooLarge
|
from werkzeug.exceptions import RequestEntityTooLarge
|
||||||
@ -11,10 +12,12 @@ from ..activities.utils_files import get_absolute_file_path
|
|||||||
from .models import User
|
from .models import User
|
||||||
from .utils import (
|
from .utils import (
|
||||||
authenticate,
|
authenticate,
|
||||||
|
check_passwords,
|
||||||
display_readable_file_size,
|
display_readable_file_size,
|
||||||
register_controls,
|
register_controls,
|
||||||
verify_extension_and_size,
|
verify_extension_and_size,
|
||||||
)
|
)
|
||||||
|
from .utils_token import decode_user_token
|
||||||
|
|
||||||
auth_blueprint = Blueprint('auth', __name__)
|
auth_blueprint = Blueprint('auth', __name__)
|
||||||
|
|
||||||
@ -464,11 +467,10 @@ def edit_user(auth_user_id):
|
|||||||
weekm = post_data.get('weekm')
|
weekm = post_data.get('weekm')
|
||||||
|
|
||||||
if password is not None and password != '':
|
if password is not None and password != '':
|
||||||
if password_conf != password:
|
message = check_passwords(password, password_conf)
|
||||||
message = 'Password and password confirmation don\'t match.\n'
|
if message != '':
|
||||||
response_object = {'status': 'error', 'message': message}
|
response_object = {'status': 'error', 'message': message}
|
||||||
return jsonify(response_object), 400
|
return jsonify(response_object), 400
|
||||||
else:
|
|
||||||
password = bcrypt.generate_password_hash(
|
password = bcrypt.generate_password_hash(
|
||||||
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||||
).decode()
|
).decode()
|
||||||
@ -657,7 +659,7 @@ def del_picture(auth_user_id):
|
|||||||
return jsonify(response_object), 500
|
return jsonify(response_object), 500
|
||||||
|
|
||||||
|
|
||||||
@auth_blueprint.route('/auth/password-reset/request', methods=['POST'])
|
@auth_blueprint.route('/auth/password/reset-request', methods=['POST'])
|
||||||
def request_password_reset():
|
def request_password_reset():
|
||||||
"""
|
"""
|
||||||
handle password reset request
|
handle password reset request
|
||||||
@ -666,7 +668,7 @@ def request_password_reset():
|
|||||||
|
|
||||||
.. sourcecode:: http
|
.. sourcecode:: http
|
||||||
|
|
||||||
POST /api/auth/password-reset/request HTTP/1.1
|
POST /api/auth/password/reset-request HTTP/1.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@ -685,7 +687,6 @@ def request_password_reset():
|
|||||||
|
|
||||||
:statuscode 200: Password reset request processed.
|
:statuscode 200: Password reset request processed.
|
||||||
:statuscode 400: Invalid payload.
|
:statuscode 400: Invalid payload.
|
||||||
:statuscode 500: Error. Please try again or contact the administrator.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
post_data = request.get_json()
|
post_data = request.get_json()
|
||||||
@ -696,9 +697,109 @@ def request_password_reset():
|
|||||||
|
|
||||||
user = User.query.filter(User.email == email).first()
|
user = User.query.filter(User.email == email).first()
|
||||||
if user:
|
if user:
|
||||||
password_reset_token = user.encode_auth_token(user.id)
|
password_reset_token = user.encode_password_reset_token(user.id)
|
||||||
|
ui_url = current_app.config['UI_URL']
|
||||||
|
email_data = {
|
||||||
|
'username': user.username,
|
||||||
|
'password_reset_url': (
|
||||||
|
f'{ui_url}/password-reset?token={password_reset_token.decode()}' # noqa
|
||||||
|
),
|
||||||
|
'operating_system': request.user_agent.platform,
|
||||||
|
'browser_name': request.user_agent.browser,
|
||||||
|
}
|
||||||
|
email_service.send(
|
||||||
|
template='password_reset_request',
|
||||||
|
lang=user.language if user.language else 'en',
|
||||||
|
recipient=user.email,
|
||||||
|
data=email_data,
|
||||||
|
)
|
||||||
response_object = {
|
response_object = {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'Password reset request processed.',
|
'message': 'Password reset request processed.',
|
||||||
}
|
}
|
||||||
return jsonify(response_object), 200
|
return jsonify(response_object), 200
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/password/update', methods=['POST'])
|
||||||
|
def update_password():
|
||||||
|
"""
|
||||||
|
update user password
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/auth/password/update HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Password updated.",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
:<json string password: password (8 characters required)
|
||||||
|
:<json string password_conf: password confirmation
|
||||||
|
:<json string token: password reset token
|
||||||
|
|
||||||
|
:statuscode 200: Password updated.
|
||||||
|
:statuscode 400: Invalid payload.
|
||||||
|
:statuscode 401: Invalid token.
|
||||||
|
:statuscode 500: Error. Please try again or contact the administrator.
|
||||||
|
|
||||||
|
"""
|
||||||
|
post_data = request.get_json()
|
||||||
|
if (
|
||||||
|
not post_data
|
||||||
|
or post_data.get('password') is None
|
||||||
|
or post_data.get('password_conf') is None
|
||||||
|
or post_data.get('token') is None
|
||||||
|
):
|
||||||
|
response_object = {'status': 'error', 'message': 'Invalid payload.'}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
password = post_data.get('password')
|
||||||
|
password_conf = post_data.get('password_conf')
|
||||||
|
token = post_data.get('token')
|
||||||
|
|
||||||
|
invalid_token_response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Invalid token. Please request a new token.',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
user_id = decode_user_token(token)
|
||||||
|
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
|
||||||
|
return jsonify(invalid_token_response_object), 401
|
||||||
|
|
||||||
|
message = check_passwords(password, password_conf)
|
||||||
|
if message != '':
|
||||||
|
response_object = {'status': 'error', 'message': message}
|
||||||
|
return jsonify(response_object), 400
|
||||||
|
|
||||||
|
user = User.query.filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
return jsonify(invalid_token_response_object), 401
|
||||||
|
try:
|
||||||
|
user.password = bcrypt.generate_password_hash(
|
||||||
|
password, current_app.config.get('BCRYPT_LOG_ROUNDS')
|
||||||
|
).decode()
|
||||||
|
db.session.commit()
|
||||||
|
response_object = {
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Password updated.',
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 200
|
||||||
|
|
||||||
|
except (exc.OperationalError, ValueError) as e:
|
||||||
|
db.session.rollback()
|
||||||
|
appLog.error(e)
|
||||||
|
response_object = {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Error. Please try again or contact the administrator.',
|
||||||
|
}
|
||||||
|
return jsonify(response_object), 500
|
||||||
|
@ -16,16 +16,22 @@ def is_valid_email(email):
|
|||||||
return re.match(mail_pattern, email) is not None
|
return re.match(mail_pattern, email) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def check_passwords(password, password_conf):
|
||||||
|
ret = ''
|
||||||
|
if password_conf != password:
|
||||||
|
ret = 'Password and password confirmation don\'t match.\n'
|
||||||
|
if len(password) < 8:
|
||||||
|
ret += 'Password: 8 characters required.\n'
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def register_controls(username, email, password, password_conf):
|
def register_controls(username, email, password, password_conf):
|
||||||
ret = ''
|
ret = ''
|
||||||
if not 2 < len(username) < 13:
|
if not 2 < len(username) < 13:
|
||||||
ret += 'Username: 3 to 12 characters required.\n'
|
ret += 'Username: 3 to 12 characters required.\n'
|
||||||
if not is_valid_email(email):
|
if not is_valid_email(email):
|
||||||
ret += 'Valid email must be provided.\n'
|
ret += 'Valid email must be provided.\n'
|
||||||
if password != password_conf:
|
ret += check_passwords(password, password_conf)
|
||||||
ret += 'Password and password confirmation don\'t match.\n'
|
|
||||||
if len(password) < 8:
|
|
||||||
ret += 'Password: 8 characters required.\n'
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
18
fittrackee_api/poetry.lock
generated
18
fittrackee_api/poetry.lock
generated
@ -285,6 +285,18 @@ version = "2.4.1"
|
|||||||
Flask = ">=0.10"
|
Flask = ">=0.10"
|
||||||
SQLAlchemy = ">=0.8.0"
|
SQLAlchemy = ">=0.8.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Let your Python tests travel through time"
|
||||||
|
name = "freezegun"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
version = "0.3.15"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
python-dateutil = ">=1.0,<2.0 || >2.0"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "GPX file parser and GPS track manipulation library"
|
description = "GPX file parser and GPS track manipulation library"
|
||||||
@ -983,7 +995,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
|||||||
testing = ["jaraco.itertools", "func-timeout"]
|
testing = ["jaraco.itertools", "func-timeout"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "eb952aaba2a4049373b197f711474741675ac9029129bdcf57b238bc466a15c6"
|
content-hash = "4199f7c3f0fe738bf7d846017b57e9903aff0b4697a8570be54a9ed63bd20306"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
@ -1169,6 +1181,10 @@ flask-sqlalchemy = [
|
|||||||
{file = "Flask-SQLAlchemy-2.4.1.tar.gz", hash = "sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"},
|
{file = "Flask-SQLAlchemy-2.4.1.tar.gz", hash = "sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"},
|
||||||
{file = "Flask_SQLAlchemy-2.4.1-py2.py3-none-any.whl", hash = "sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327"},
|
{file = "Flask_SQLAlchemy-2.4.1-py2.py3-none-any.whl", hash = "sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327"},
|
||||||
]
|
]
|
||||||
|
freezegun = [
|
||||||
|
{file = "freezegun-0.3.15-py2.py3-none-any.whl", hash = "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2"},
|
||||||
|
{file = "freezegun-0.3.15.tar.gz", hash = "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b"},
|
||||||
|
]
|
||||||
gpxpy = [
|
gpxpy = [
|
||||||
{file = "gpxpy-1.3.4.tar.gz", hash = "sha256:4a0f072ae5bdf9270c7450e452f93a6c5c91d888114e8d78868a8f163b0dbb15"},
|
{file = "gpxpy-1.3.4.tar.gz", hash = "sha256:4a0f072ae5bdf9270c7450e452f93a6c5c91d888114e8d78868a8f163b0dbb15"},
|
||||||
]
|
]
|
||||||
|
@ -32,6 +32,7 @@ sphinxcontrib-httpdomain = "^1.7"
|
|||||||
sphinx-bootstrap-theme = "^0.7.1"
|
sphinx-bootstrap-theme = "^0.7.1"
|
||||||
recommonmark = "^0.6.0"
|
recommonmark = "^0.6.0"
|
||||||
pyopenssl = "^19.0"
|
pyopenssl = "^19.0"
|
||||||
|
freezegun = "^0.3.15"
|
||||||
|
|
||||||
[tool.pytest]
|
[tool.pytest]
|
||||||
norecursedirs = "fittrackee_api/.venv"
|
norecursedirs = "fittrackee_api/.venv"
|
||||||
|
Loading…
Reference in New Issue
Block a user