add token to user registration and admin email

This commit is contained in:
Cayo Puigdefabregas 2022-10-19 18:55:34 +02:00
parent 9e86f0e3ae
commit c93c143cc8
7 changed files with 243 additions and 100 deletions

View file

@ -1,5 +1,5 @@
from flask import g
from flask import current_app as app from flask import current_app as app
from flask import g
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from wtforms import ( from wtforms import (
@ -115,17 +115,16 @@ class PasswordForm(FlaskForm):
class UserNewRegisterForm(FlaskForm): class UserNewRegisterForm(FlaskForm):
email = EmailField( email = EmailField(
'Email Address', [ 'Email Address', [validators.DataRequired(), validators.Length(min=6, max=35)]
validators.DataRequired(),
validators.Length(min=6, max=35)
]
) )
password = PasswordField('Password', [validators.DataRequired()]) password = PasswordField('Password', [validators.DataRequired()])
password2 = PasswordField('Password', [validators.DataRequired()]) password2 = PasswordField('Password', [validators.DataRequired()])
name = StringField( name = StringField(
'Name', [validators.DataRequired(), validators.Length(min=3, max=35)] 'Name', [validators.DataRequired(), validators.Length(min=3, max=35)]
) )
telephone = TelField('Telephone', [validators.DataRequired()]) telephone = TelField(
'Telephone', [validators.DataRequired(), validators.Length(min=9, max=35)]
)
error_messages = { error_messages = {
'invalid_login': ( 'invalid_login': (
@ -160,16 +159,19 @@ class UserNewRegisterForm(FlaskForm):
return True return True
def save(self, commit=True): def save(self, commit=True):
user = User( user_validation = self.new_user()
email=self.email.data, if commit:
password=self.password.data, db.session.commit()
active=False
) self._token = user_validation.token
self.send_mail()
self.send_mail_admin(user_validation.user)
def new_user(self):
user = User(email=self.email.data, password=self.password.data, active=False)
person = Person( person = Person(
email=self.email.data, email=self.email.data, name=self.name.data, telephone=self.telephone.data
name=self.name.data,
telephone=self.telephone.data
) )
user.individuals.add(person) user.individuals.add(person)
@ -180,19 +182,33 @@ class UserNewRegisterForm(FlaskForm):
) )
db.session.add(user_validation) db.session.add(user_validation)
if commit: return user_validation
db.session.commit()
self._token = user_validation.token
self.send_mail()
def send_mail(self): def send_mail(self):
host = app.config.get('HOST') host = app.config.get('HOST')
token = self._token token = self._token
url = f'https://{ host }/validate/{ token }' url = f'https://{ host }/validate_user/{ token }'
message = """Hello, you are register in Usody.com message = """
Hello, you are register in Usody.com
Please for activate your account click in the next address: """ Please for activate your account click in the next address: """
message += url message += url
subject = "Validate email for register in Usody.com" subject = "Validate email for register in Usody.com"
send_email(subject, [self.email.data], message) send_email(subject, [self.email.data], message)
def send_mail_admin(self, user):
person = next(iter(user.individuals))
email = person.email
name = person.name
telephone = person.telephone
message = f"""A new user has been registered. These are your data"
Name: {name}
Telephone: {telephone}
Email: {email}
"""
subject = "New Register"
email_admin = app.config.get("EMAIL_ADMIN")
if email_admin:
send_email(subject, [email_admin], message)

View file

@ -187,10 +187,6 @@ class FilterForm(FlaskForm):
if filter_type: if filter_type:
self.devices = self.devices.filter(Device.type.in_(filter_type)) self.devices = self.devices.filter(Device.type.in_(filter_type))
# if self.device_type in STORAGE + ["All DataStorage"]:
# import pdb; pdb.set_trace()
# self.devices = self.devices.filter(Component.parent_id.is_(None))
return self.devices.order_by(Device.updated.desc()) return self.devices.order_by(Device.updated.desc())

View file

@ -14,21 +14,20 @@ from __future__ import with_statement
__version__ = '0.9.1' __version__ = '0.9.1'
import re import re
import blinker
import smtplib import smtplib
import sys import sys
import time import time
import unicodedata import unicodedata
from contextlib import contextmanager
from email import charset from email import charset
from email.encoders import encode_base64 from email.encoders import encode_base64
from email.header import Header
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.header import Header from email.utils import formataddr, formatdate, make_msgid, parseaddr
from email.utils import formatdate, formataddr, make_msgid, parseaddr
from contextlib import contextmanager
import blinker
from flask import current_app from flask import current_app
PY3 = sys.version_info[0] == 3 PY3 = sys.version_info[0] == 3
@ -36,12 +35,13 @@ PY3 = sys.version_info[0] == 3
PY34 = PY3 and sys.version_info[1] >= 4 PY34 = PY3 and sys.version_info[1] >= 4
if PY3: if PY3:
string_types = str, string_types = (str,)
text_type = str text_type = str
from email import policy from email import policy
message_policy = policy.SMTP message_policy = policy.SMTP
else: else:
string_types = basestring, string_types = (basestring,)
text_type = unicode text_type = unicode
message_policy = None message_policy = None
@ -85,10 +85,10 @@ def force_text(s, encoding='utf-8', errors='strict'):
if not isinstance(s, Exception): if not isinstance(s, Exception):
raise FlaskMailUnicodeDecodeError(s, *e.args) raise FlaskMailUnicodeDecodeError(s, *e.args)
else: else:
s = ' '.join([force_text(arg, encoding, strings_only, s = ' '.join([force_text(arg, encoding, strings_only, errors) for arg in s])
errors) for arg in s])
return s return s
def sanitize_subject(subject, encoding='utf-8'): def sanitize_subject(subject, encoding='utf-8'):
try: try:
subject.encode('ascii') subject.encode('ascii')
@ -99,6 +99,7 @@ def sanitize_subject(subject, encoding='utf-8'):
subject = Header(subject, 'utf-8').encode() subject = Header(subject, 'utf-8').encode()
return subject return subject
def sanitize_address(addr, encoding='utf-8'): def sanitize_address(addr, encoding='utf-8'):
if isinstance(addr, string_types): if isinstance(addr, string_types):
addr = parseaddr(force_text(addr)) addr = parseaddr(force_text(addr))
@ -131,6 +132,7 @@ def _has_newline(line):
return True return True
return False return False
class Connection(object): class Connection(object):
"""Handles connection to host.""" """Handles connection to host."""
@ -176,7 +178,8 @@ class Connection(object):
assert message.sender, ( assert message.sender, (
"The message does not specify a sender and a default sender " "The message does not specify a sender and a default sender "
"has not been configured") "has not been configured"
)
if message.has_bad_headers(): if message.has_bad_headers():
raise BadHeaderError raise BadHeaderError
@ -185,11 +188,13 @@ class Connection(object):
message.date = time.time() message.date = time.time()
if self.host: if self.host:
self.host.sendmail(sanitize_address(envelope_from or message.sender), self.host.sendmail(
sanitize_address(envelope_from or message.sender),
list(sanitize_addresses(message.send_to)), list(sanitize_addresses(message.send_to)),
message.as_bytes() if PY3 else message.as_string(), message.as_bytes() if PY3 else message.as_string(),
message.mail_options, message.mail_options,
message.rcpt_options) message.rcpt_options,
)
email_dispatched.send(message, app=current_app._get_current_object()) email_dispatched.send(message, app=current_app._get_current_object())
@ -227,8 +232,14 @@ class Attachment(object):
:param disposition: content-disposition (if any) :param disposition: content-disposition (if any)
""" """
def __init__(self, filename=None, content_type=None, data=None, def __init__(
disposition=None, headers=None): self,
filename=None,
content_type=None,
data=None,
disposition=None,
headers=None,
):
self.filename = filename self.filename = filename
self.content_type = content_type self.content_type = content_type
self.data = data self.data = data
@ -255,7 +266,9 @@ class Message(object):
:param rcpt_options: A list of ESMTP options to be used in RCPT commands :param rcpt_options: A list of ESMTP options to be used in RCPT commands
""" """
def __init__(self, subject='', def __init__(
self,
subject='',
recipients=None, recipients=None,
body=None, body=None,
html=None, html=None,
@ -268,7 +281,8 @@ class Message(object):
charset=None, charset=None,
extra_headers=None, extra_headers=None,
mail_options=None, mail_options=None,
rcpt_options=None): rcpt_options=None,
):
sender = sender or current_app.extensions['mail'].default_sender sender = sender or current_app.extensions['mail'].default_sender
@ -364,9 +378,9 @@ class Message(object):
filename = filename.encode('utf8') filename = filename.encode('utf8')
filename = ('UTF8', '', filename) filename = ('UTF8', '', filename)
f.add_header('Content-Disposition', f.add_header(
attachment.disposition, 'Content-Disposition', attachment.disposition, filename=filename
filename=filename) )
for key, value in attachment.headers: for key, value in attachment.headers:
f.add_header(key, value) f.add_header(key, value)
@ -418,7 +432,10 @@ class Message(object):
def is_bad_headers(self): def is_bad_headers(self):
from warnings import warn from warnings import warn
msg = 'is_bad_headers is deprecated, use the new has_bad_headers method instead.'
msg = (
'is_bad_headers is deprecated, use the new has_bad_headers method instead.'
)
warn(DeprecationWarning(msg), stacklevel=1) warn(DeprecationWarning(msg), stacklevel=1)
return self.has_bad_headers() return self.has_bad_headers()
@ -435,12 +452,14 @@ class Message(object):
self.recipients.append(recipient) self.recipients.append(recipient)
def attach(self, def attach(
self,
filename=None, filename=None,
content_type=None, content_type=None,
data=None, data=None,
disposition=None, disposition=None,
headers=None): headers=None,
):
"""Adds an attachment to the message. """Adds an attachment to the message.
:param filename: filename of attachment :param filename: filename of attachment
@ -449,11 +468,11 @@ class Message(object):
:param disposition: content-disposition (if any) :param disposition: content-disposition (if any)
""" """
self.attachments.append( self.attachments.append(
Attachment(filename, content_type, data, disposition, headers)) Attachment(filename, content_type, data, disposition, headers)
)
class _MailMixin(object): class _MailMixin(object):
@contextmanager @contextmanager
def record_messages(self): def record_messages(self):
"""Records all messages. Use in unit tests for example:: """Records all messages. Use in unit tests for example::
@ -508,13 +527,26 @@ class _MailMixin(object):
try: try:
return Connection(app.extensions['mail']) return Connection(app.extensions['mail'])
except KeyError: except KeyError:
raise RuntimeError("The curent application was not configured with Flask-Mail") raise RuntimeError(
"The curent application was not configured with Flask-Mail"
)
class _Mail(_MailMixin): class _Mail(_MailMixin):
def __init__(self, server, username, password, port, use_tls, use_ssl, def __init__(
default_sender, debug, max_emails, suppress, self,
ascii_attachments=False): server,
username,
password,
port,
use_tls,
use_ssl,
default_sender,
debug,
max_emails,
suppress,
ascii_attachments=False,
):
self.server = server self.server = server
self.username = username self.username = username
self.password = password self.password = password
@ -553,7 +585,7 @@ class Mail(_MailMixin):
int(config.get('MAIL_DEBUG', debug)), int(config.get('MAIL_DEBUG', debug)),
config.get('MAIL_MAX_EMAILS'), config.get('MAIL_MAX_EMAILS'),
config.get('MAIL_SUPPRESS_SEND', testing), config.get('MAIL_SUPPRESS_SEND', testing),
config.get('MAIL_ASCII_ATTACHMENTS', False) config.get('MAIL_ASCII_ATTACHMENTS', False),
) )
def init_app(self, app): def init_app(self, app):
@ -577,7 +609,10 @@ class Mail(_MailMixin):
signals = blinker.Namespace() signals = blinker.Namespace()
email_dispatched = signals.signal("email-dispatched", doc=""" email_dispatched = signals.signal(
"email-dispatched",
doc="""
Signal sent when an email is dispatched. This signal will also be sent Signal sent when an email is dispatched. This signal will also be sent
in testing mode, even though the email will not actually be sent. in testing mode, even though the email will not actually be sent.
""") """,
)

View file

@ -41,6 +41,13 @@
<label for="yourEmail" class="form-label">Email</label> <label for="yourEmail" class="form-label">Email</label>
<input type="email" name="email" class="form-control" id="yourEmail" required value="{{ form.email.data|default('', true) }}"> <input type="email" name="email" class="form-control" id="yourEmail" required value="{{ form.email.data|default('', true) }}">
<div class="invalid-feedback">Please enter your email.</div> <div class="invalid-feedback">Please enter your email.</div>
{% if form.email.errors %}
<p class="text-danger">
{% for error in form.email.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div> </div>
<div class="col-12"> <div class="col-12">
@ -58,26 +65,41 @@
<div class="col-12"> <div class="col-12">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input name="name" class="form-control" id="name" required> <input name="name" class="form-control" id="name" required>
{% if form.name.errors %}
<p class="text-danger">
{% for error in form.name.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div> </div>
<div class="col-12"> <div class="col-12">
<label for="telephone" class="form-label">Telephone</label> <label for="telephone" class="form-label">Telephone</label>
<input type="tel" name="telephone" class="form-control" id="telephone" required> <input type="tel" name="telephone" class="form-control" id="telephone" required>
{% if form.telephone.errors %}
<p class="text-danger">
{% for error in form.telephone.errors %}
{{ error }}<br/>
{% endfor %}
</p>
{% endif %}
</div> </div>
<div class="col-12"> <div class="col-12">
<button class="btn btn-primary w-100" type="submit">Register</button> <button class="btn btn-primary w-100" type="submit">Register</button>
</div> </div>
<div class="col-12"> <div class="col-12">
<p class="small mb-0">Don't have account? <a href="#TODO" onclick="$('#exampleModal').modal('show')" data-toggle="modal" data-target="#exampleModal">Create an account</a></p> <p class="small mb-0">
You have account? <a href="{{ url_for('core.login') }}">Create an account</a>
</p>
</div> </div>
</form> </form>
{% else %} {% else %}
<div class="col-12">
We have sent you a validation email. Please check your email.
</div> </div>
<div class="col-12"> <div class="col-12 p-4">
{{ form._token }} We have sent you a validation email.<br />
Please check your email.
</div> </div>
{% endif %} {% endif %}
@ -96,18 +118,4 @@
</div> </div>
</main><!-- End #main --> </main><!-- End #main -->
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Do you want to try USOdy tools?</h5>
</div>
<div class="modal-body">
Just write an email to <a href="mali:hello@usody.com">hello@usody.com</a>
</div>
</div>
</div>
</div> <!-- End register modal -->
{% endblock body %} {% endblock body %}

View file

@ -0,0 +1,66 @@
{% extends "ereuse_devicehub/base.html" %}
{% block page_title %}Login{% endblock %}
{% block body %}
<main>
<div class="container">
<section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
<div class="d-flex justify-content-center py-4">
<a href="{{ url_for('core.login') }}" class="logo d-flex align-items-center w-auto">
<img src="{{ url_for('static', filename='img/logo_usody_clock.png') }}" alt="">
</a>
</div><!-- End Logo -->
<div class="card mb-3">
<div class="card-body">
{% if is_valid %}
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">User is valid</h5>
<div class="col-12">
Your new user is activate.</br />
Now you can do <a style="color: #cc0066;" href="{{ url_for('core.login') }}">Login</a> and entry.
</div>
</div>
{% else %}
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4">User is Invalid</h5>
<div class="row">
<div class="col-12 text-center">
<span class="text-danger">Invalid</span>
</div>
</div>
<div class="row">
<div class="col-12">
Sorry, your token not exist or is expired.
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="credits">
Designed by <a href="https://bootstrapmade.com/">BootstrapMade</a>
</div>
</div>
</div>
</div>
</section>
</div>
</main><!-- End #main -->
{% endblock body %}

View file

@ -9,7 +9,7 @@ from ereuse_devicehub.db import db
from ereuse_devicehub.forms import LoginForm, PasswordForm, UserNewRegisterForm from ereuse_devicehub.forms import LoginForm, PasswordForm, UserNewRegisterForm
from ereuse_devicehub.resources.action.models import Trade from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User, UserValidation
from ereuse_devicehub.utils import is_safe_url from ereuse_devicehub.utils import is_safe_url
core = Blueprint('core', __name__) core = Blueprint('core', __name__)
@ -120,10 +120,32 @@ class UserRegistrationView(View):
return flask.render_template(self.template_name, **context) return flask.render_template(self.template_name, **context)
class UserValidationView(View):
methods = ['GET']
template_name = 'ereuse_devicehub/user_validation.html'
def dispatch_request(self, token):
context = {'is_valid': self.is_valid(token), 'version': __version__}
return flask.render_template(self.template_name, **context)
def is_valid(self, token):
user_valid = UserValidation.query.filter_by(token=token).first()
if not user_valid:
return False
user = user_valid.user
user.active = True
db.session.commit()
return True
core.add_url_rule('/login/', view_func=LoginView.as_view('login')) core.add_url_rule('/login/', view_func=LoginView.as_view('login'))
core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout')) core.add_url_rule('/logout/', view_func=LogoutView.as_view('logout'))
core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile')) core.add_url_rule('/profile/', view_func=UserProfileView.as_view('user-profile'))
core.add_url_rule( core.add_url_rule(
'/new_register/', view_func=UserRegistrationView.as_view('user-registration') '/new_register/', view_func=UserRegistrationView.as_view('user-registration')
) )
core.add_url_rule(
'/validate_user/<uuid:token>',
view_func=UserValidationView.as_view('user-validation'),
)
core.add_url_rule('/set_password/', view_func=UserPasswordView.as_view('set-password')) core.add_url_rule('/set_password/', view_func=UserPasswordView.as_view('set-password'))

View file

@ -6,12 +6,12 @@ Use this as a starting point.
from decouple import config from decouple import config
from ereuse_devicehub.mail.flask_mail import Mail
from ereuse_devicehub.api.views import api from ereuse_devicehub.api.views import api
from ereuse_devicehub.config import DevicehubConfig from ereuse_devicehub.config import DevicehubConfig
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.inventory.views import devices from ereuse_devicehub.inventory.views import devices
from ereuse_devicehub.labels.views import labels from ereuse_devicehub.labels.views import labels
from ereuse_devicehub.mail.flask_mail import Mail
from ereuse_devicehub.views import core from ereuse_devicehub.views import core
from ereuse_devicehub.workbench.views import workbench from ereuse_devicehub.workbench.views import workbench