Merged in feature/registration-confirmation (pull request #8)
Feature/registration confirmationdevelop
commit
1a9cb3f973
3
.env
3
.env
|
@ -3,3 +3,6 @@ REDIS_URL=redis://localhost:6379
|
|||
CELERY_TASK_SERIALIZER=json
|
||||
DEBUG=True
|
||||
MQTT_CLIENT=local
|
||||
APP_MAIL_USERNAME=final.iot.backend.mailer@gmail.com
|
||||
APP_MAIL_PASSWORD=FinalIoT1234
|
||||
FLASK_ENV=development
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import datetime
|
||||
from app.core import bcrypt
|
||||
from .models import Account, Role
|
||||
from .emailtoken import generate_confirmation_token, confirm_token
|
||||
|
||||
|
||||
def create_account(username, email, password):
|
||||
|
@ -12,19 +14,36 @@ def create_account(username, email, password):
|
|||
:type username: string
|
||||
:type email: string
|
||||
:type password: string
|
||||
:returns: True if account is successfully created
|
||||
:rtype: Boolean
|
||||
:returns: Email confirmation token if creation was successful
|
||||
:rtype: string
|
||||
:raises: ValueError if account already exists
|
||||
"""
|
||||
if not Account.exists_with_any_of(username=username, email=email):
|
||||
pw_hash = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
account = Account(username, pw_hash, email)
|
||||
account.save()
|
||||
return True
|
||||
|
||||
emailtoken = generate_confirmation_token(account.email)
|
||||
return emailtoken
|
||||
|
||||
raise ValueError("Account with given parameters already exists")
|
||||
|
||||
|
||||
def confirm_email_token(token):
|
||||
try:
|
||||
email = confirm_token(token)
|
||||
except Exception:
|
||||
return False
|
||||
user = Account.query.filter_by(email=email).first_or_404()
|
||||
if user.confirmed:
|
||||
return True
|
||||
else:
|
||||
user.confirmed = True
|
||||
user.confirmed_on = datetime.datetime.now()
|
||||
user.save()
|
||||
return True
|
||||
|
||||
|
||||
def update_account_role(account_id, role_id):
|
||||
"""
|
||||
Tries to update account role
|
||||
|
@ -99,6 +118,10 @@ def create_token(username, password):
|
|||
if not bcrypt.check_password_hash(account.password, password):
|
||||
raise ValueError("Invalid credentials")
|
||||
|
||||
if not account.confirmed:
|
||||
print('ACCOUNT NOT CONFIRMED?')
|
||||
raise ValueError("Email not confirmed")
|
||||
|
||||
return account.create_auth_token()
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from flask_mail import Message
|
||||
|
||||
|
||||
def send_email(mailmanager, to, subject, template):
|
||||
msg = Message(
|
||||
subject,
|
||||
recipients=[to],
|
||||
html=template
|
||||
)
|
||||
mailmanager.send(msg)
|
|
@ -0,0 +1,21 @@
|
|||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
from app.core import app
|
||||
|
||||
|
||||
def generate_confirmation_token(email):
|
||||
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
|
||||
return serializer.dumps(email, salt=app.config['SECURITY_PASSWORD_SALT'])
|
||||
|
||||
|
||||
def confirm_token(token, expiration=3600):
|
||||
serializer = URLSafeTimedSerializer(app.config['SECRET_KEY'])
|
||||
try:
|
||||
email = serializer.loads(
|
||||
token,
|
||||
salt=app.config['SECURITY_PASSWORD_SALT'],
|
||||
max_age=expiration
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
return email
|
|
@ -13,6 +13,8 @@ class Account(db.Model):
|
|||
email = db.Column(db.String, index=True, unique=True)
|
||||
role_id = db.Column(db.Integer, db.ForeignKey("roles.id"))
|
||||
role = db.relationship("Role", foreign_keys=[role_id])
|
||||
confirmed = db.Column(db.Boolean, default=False, nullable=False)
|
||||
confirmed_at = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime,
|
||||
nullable=False,
|
||||
default=db.func.current_timestamp())
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from app.celery_builder import task_builder
|
||||
from app.accounts.email import send_email
|
||||
from flask import current_app as app
|
||||
from flask_mail import Mail
|
||||
|
||||
|
||||
@task_builder.task()
|
||||
def send_email_task(to, subject, template):
|
||||
mailmanager = Mail(app)
|
||||
send_email(mailmanager, to, subject, template)
|
|
@ -12,7 +12,9 @@ def add_resources():
|
|||
AccountListResource,
|
||||
AccountRoleResource,
|
||||
RoleResource,
|
||||
RolesResource)
|
||||
RolesResource,
|
||||
AccountEmailTokenResource,
|
||||
AccountEmailTokenResendResource)
|
||||
from .resources.token import TokenResource, ValidateTokenResource
|
||||
from .resources.device import (DeviceResource,
|
||||
DeviceRecordingResource,
|
||||
|
@ -25,6 +27,10 @@ def add_resources():
|
|||
api.add_resource(AccountResource, '/v1/accounts/<int:account_id>')
|
||||
api.add_resource(AccountListResource, '/v1/accounts')
|
||||
api.add_resource(AccountRoleResource, '/v1/accounts/<int:account_id>/role')
|
||||
api.add_resource(AccountEmailTokenResource,
|
||||
'/v1/email/confirm/<string:token>')
|
||||
api.add_resource(AccountEmailTokenResendResource,
|
||||
'/v1/email/resend')
|
||||
api.add_resource(RoleResource, '/v1/roles/<int:role_id>')
|
||||
api.add_resource(RolesResource, '/v1/roles')
|
||||
api.add_resource(TokenResource, '/v1/token')
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from flask_restful import Resource, abort
|
||||
from flask import g
|
||||
from flask import g, render_template
|
||||
from marshmallow import Schema, fields
|
||||
from webargs.flaskparser import use_args
|
||||
from flasgger import swag_from
|
||||
from app.api.blueprint import api
|
||||
import app.accounts.api as accounts
|
||||
from app.accounts.tasks import send_email_task
|
||||
from app.api.auth_protection import ProtectedResource
|
||||
from app.api.permission_protection import (requires_permission,
|
||||
valid_permissions)
|
||||
|
@ -85,11 +87,34 @@ class AccountListResource(Resource):
|
|||
@swag_from('swagger/create_account_spec.yaml')
|
||||
def post(self, args):
|
||||
try:
|
||||
success = accounts.create_account(
|
||||
emailtoken = accounts.create_account(
|
||||
args['username'],
|
||||
args['email'],
|
||||
args['password'])
|
||||
if success:
|
||||
return '', 201
|
||||
confirm_url = api.url_for(
|
||||
AccountEmailTokenResource,
|
||||
token=emailtoken, _external=True)
|
||||
html = render_template(
|
||||
'activate_mail.html',
|
||||
confirm_url=confirm_url)
|
||||
send_email_task.delay(
|
||||
args['email'],
|
||||
'Please confirm your email',
|
||||
html)
|
||||
return '', 201
|
||||
except ValueError:
|
||||
abort(422, message='Account already exists', status='error')
|
||||
|
||||
|
||||
class AccountEmailTokenResource(Resource):
|
||||
def get(self, token):
|
||||
success = accounts.confirm_email_token(token)
|
||||
if success:
|
||||
return '{"status": "success", \
|
||||
"message": "Successfully confirmed email"}', 200
|
||||
|
||||
|
||||
class AccountEmailTokenResendResource(Resource):
|
||||
@use_args(UserSchema(), locations=('json',))
|
||||
def post(self, args):
|
||||
return '', 201
|
||||
|
|
|
@ -23,8 +23,8 @@ class TokenResource(Resource):
|
|||
args['password'])
|
||||
if token:
|
||||
return {'status': 'success', 'token': token}, 200
|
||||
except ValueError:
|
||||
abort(401, message='Invalid credentials', status='error')
|
||||
except ValueError, e:
|
||||
abort(401, message=str(e), status='error')
|
||||
|
||||
|
||||
class ValidateTokenResource(ProtectedResource):
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# App initialization
|
||||
from flask import Flask
|
||||
from .tasks.celery_configurator import make_celery
|
||||
from app.tasks.celery_configurator import make_celery
|
||||
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_object('config')
|
||||
app.config.from_pyfile('config.py', silent=True)
|
||||
app.config['MQTT_CLIENT_ID'] = app.config['MQTT_CLIENT_ID'] + '-worker'
|
||||
task_builder = make_celery(app)
|
||||
task_builder.autodiscover_tasks(['app.devices'])
|
||||
task_builder.autodiscover_tasks(['app.devices', 'app.accounts'])
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from flask_api import FlaskAPI
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_mail import Mail
|
||||
from flasgger import Swagger
|
||||
from flask_cors import CORS
|
||||
from .tasks import celery_configurator
|
||||
|
@ -11,6 +12,7 @@ app.config.from_object('config')
|
|||
app.config.from_pyfile('config.py', silent=True)
|
||||
db = SQLAlchemy(app)
|
||||
bcrypt = Bcrypt(app)
|
||||
mail = Mail(app)
|
||||
swagger = Swagger(app, template_file='swagger/template.yaml')
|
||||
swagger.template['info']['version'] = app.config['APP_VERSION']
|
||||
CORS(app)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p>
|
||||
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
|
||||
<br>
|
||||
<p>Cheers!</p>
|
15
config.py
15
config.py
|
@ -27,6 +27,7 @@ CSRF_SESSION_KEY = "secret"
|
|||
|
||||
# Secret key for signing cookies
|
||||
SECRET_KEY = "?['Z(Z\x83Y \x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld"
|
||||
SECURITY_PASSWORD_SALT = "IyoZvOJb4feT3xKlYXyOJveHSIY4GDg6"
|
||||
|
||||
# MQTT configuration
|
||||
MQTT_CLIENT_ID = 'final-iot-backend-server-' + os.environ['MQTT_CLIENT']
|
||||
|
@ -40,6 +41,20 @@ MQTT_REFRESH_TIME = 1.0 # refresh time in seconds
|
|||
CELERY_BROKER_URL = os.environ['REDIS_URL']
|
||||
CELERY_RESULT_BACKEND = os.environ['REDIS_URL']
|
||||
|
||||
# Mailer config
|
||||
MAIL_SERVER = 'smtp.googlemail.com'
|
||||
MAIL_PORT = 465
|
||||
MAIL_USE_TLS = False
|
||||
MAIL_USE_SSL = True
|
||||
MAIL_DEBUG = False
|
||||
|
||||
# gmail authentication
|
||||
MAIL_USERNAME = os.environ['APP_MAIL_USERNAME']
|
||||
MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
|
||||
|
||||
# mail accounts
|
||||
MAIL_DEFAULT_SENDER = 'final.iot.backend.mailer@gmail.com'
|
||||
|
||||
# Flasgger config
|
||||
SWAGGER = {
|
||||
'uiversion': 3
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: ae43d7caad52
|
||||
Revises: fd5cfe0f1c51
|
||||
Create Date: 2018-10-10 21:01:06.071591
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ae43d7caad52'
|
||||
down_revision = 'fd5cfe0f1c51'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
accounts = sa.table('accounts', sa.column('id', sa.Integer),
|
||||
sa.column('confirmed', sa.BOOLEAN()))
|
||||
op.execute(accounts.update().
|
||||
values({'confirmed': True}))
|
||||
|
||||
op.alter_column('accounts', 'confirmed',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('accounts', 'confirmed',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,40 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: fd5cfe0f1c51
|
||||
Revises: 55611f1868bd
|
||||
Create Date: 2018-10-08 23:07:48.054401
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fd5cfe0f1c51'
|
||||
down_revision = '55611f1868bd'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('accounts', sa.Column('confirmed', sa.Boolean(), nullable=True))
|
||||
op.add_column('accounts', sa.Column('confirmed_at', sa.DateTime(), nullable=True))
|
||||
|
||||
role = sa.table('roles', sa.column('id', sa.Integer),
|
||||
sa.column('permissions', sa.ARRAY(sa.String)))
|
||||
op.execute(role.update().where(role.c.id == op.inline_literal(1)).
|
||||
values({'permissions':
|
||||
['CREATE_DEVICE_TYPE', 'CREATE_ROLE',
|
||||
'ASSIGN_ROLE',
|
||||
'CREATE_DEVICE', 'CREATE_DASHBOARD',
|
||||
'READ_DEVICE_TYPES', 'READ_ROLES']})
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('accounts', 'confirmed_at')
|
||||
op.drop_column('accounts', 'confirmed')
|
||||
# ### end Alembic commands ###
|
|
@ -1,8 +1,10 @@
|
|||
alembic==0.9.9
|
||||
amqp==2.3.2
|
||||
aniso8601==3.0.0
|
||||
apispec==0.39.0
|
||||
bcrypt==3.1.4
|
||||
billiard==3.5.0.4
|
||||
blinker==1.4
|
||||
celery==4.2.0
|
||||
cffi==1.11.5
|
||||
click==6.7
|
||||
|
@ -11,11 +13,13 @@ Flask==1.0.2
|
|||
Flask-API==1.0
|
||||
Flask-Bcrypt==0.7.1
|
||||
Flask-Cors==3.0.4
|
||||
Flask-Mail==0.9.1
|
||||
Flask-Migrate==2.1.1
|
||||
Flask-MQTT==1.0.3
|
||||
Flask-RESTful==0.3.6
|
||||
Flask-Script==2.0.6
|
||||
Flask-SQLAlchemy==2.3.2
|
||||
functools32==3.2.3.post2
|
||||
gunicorn==19.8.1
|
||||
itsdangerous==0.24
|
||||
Jinja2==2.10
|
||||
|
|
Loading…
Reference in New Issue