aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCédric Bonhomme <cedric@cedricbonhomme.org>2016-02-18 08:59:13 +0100
committerCédric Bonhomme <cedric@cedricbonhomme.org>2016-02-18 08:59:13 +0100
commit2e5a241777ef0bb0d76420d39bf3be41e16e042a (patch)
tree3223b8fba4fa244fa97b0df0b8bf8c5b91aeffec
parentCheck if the id of the category is '0'. (diff)
downloadnewspipe-2e5a241777ef0bb0d76420d39bf3be41e16e042a.tar.gz
newspipe-2e5a241777ef0bb0d76420d39bf3be41e16e042a.tar.bz2
newspipe-2e5a241777ef0bb0d76420d39bf3be41e16e042a.zip
New management of the token for the account confirmation.
-rw-r--r--migrations/versions/ac35c979311a_removed_activation_key_from_the_user_.py28
-rw-r--r--src/bootstrap.py5
-rw-r--r--src/conf.py1
-rw-r--r--src/conf/conf.cfg-sample1
-rwxr-xr-xsrc/manager.py2
-rw-r--r--src/tests/fixtures.py2
-rw-r--r--src/web/controllers/user.py8
-rw-r--r--src/web/forms.py9
-rw-r--r--src/web/lib/user_utils.py23
-rw-r--r--src/web/models/__init__.py2
-rw-r--r--src/web/models/user.py3
-rw-r--r--src/web/notifications.py6
-rw-r--r--src/web/templates/admin/dashboard.html4
-rw-r--r--src/web/views/admin.py11
-rw-r--r--src/web/views/api/common.py3
-rw-r--r--src/web/views/user.py12
16 files changed, 85 insertions, 35 deletions
diff --git a/migrations/versions/ac35c979311a_removed_activation_key_from_the_user_.py b/migrations/versions/ac35c979311a_removed_activation_key_from_the_user_.py
new file mode 100644
index 00000000..deeb6037
--- /dev/null
+++ b/migrations/versions/ac35c979311a_removed_activation_key_from_the_user_.py
@@ -0,0 +1,28 @@
+"""removed activation_key from the user table and add enabled column
+
+Revision ID: ac35c979311a
+Revises: 661199d8768a
+Create Date: 2016-02-18 08:54:43.786641
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'ac35c979311a'
+down_revision = '661199d8768a'
+branch_labels = None
+depends_on = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.drop_column('user', 'activation_key')
+ op.add_column('user', sa.Column('enabled', sa.Boolean(), nullable=False,
+ default=False))
+
+
+def downgrade():
+ op.drop_column('user', 'enabled')
+ op.add_column('user', sa.Column('activation_key', sa.String(),
+ nullable=False, default=''))
diff --git a/src/bootstrap.py b/src/bootstrap.py
index 25528ef5..f1624111 100644
--- a/src/bootstrap.py
+++ b/src/bootstrap.py
@@ -44,6 +44,11 @@ application.config['SECRET_KEY'] = getattr(conf, 'WEBSERVER_SECRET', None)
if not application.config['SECRET_KEY']:
application.config['SECRET_KEY'] = os.urandom(12)
+application.config['SECURITY_PASSWORD_SALT'] = getattr(conf,
+ 'SECURITY_PASSWORD_SALT', None)
+if not application.config['SECURITY_PASSWORD_SALT']:
+ application.config['SECURITY_PASSWORD_SALT'] = os.urandom(12)
+
application.config['RECAPTCHA_USE_SSL'] = True
application.config['RECAPTCHA_PUBLIC_KEY'] = conf.RECAPTCHA_PUBLIC_KEY
application.config['RECAPTCHA_PRIVATE_KEY'] = conf.RECAPTCHA_PRIVATE_KEY
diff --git a/src/conf.py b/src/conf.py
index d65bb516..5c96fea8 100644
--- a/src/conf.py
+++ b/src/conf.py
@@ -77,6 +77,7 @@ ADMIN_EMAIL = config.get('misc', 'admin_email')
RECAPTCHA_PUBLIC_KEY = config.get('misc', 'recaptcha_public_key')
RECAPTCHA_PRIVATE_KEY = config.get('misc',
'recaptcha_private_key')
+SECURITY_PASSWORD_SALT = config.get('misc', 'security_password_salt')
LOG_PATH = os.path.abspath(config.get('misc', 'log_path'))
NB_WORKER = config.getint('misc', 'nb_worker')
API_LOGIN = config.get('crawler', 'api_login')
diff --git a/src/conf/conf.cfg-sample b/src/conf/conf.cfg-sample
index cc37a4a2..286ccbe5 100644
--- a/src/conf/conf.cfg-sample
+++ b/src/conf/conf.cfg-sample
@@ -9,6 +9,7 @@ platform_url = http://127.0.0.1:5000/
admin_email =
recaptcha_public_key =
recaptcha_private_key =
+security_password_salt = a secret to confirm user account
log_path = ./src/web/var/jarr.log
nb_worker = 5
log_level = info
diff --git a/src/manager.py b/src/manager.py
index 781d742b..e64263f2 100755
--- a/src/manager.py
+++ b/src/manager.py
@@ -64,7 +64,7 @@ def fetch_asyncio(user_id, feed_id):
loop = asyncio.get_event_loop()
for user in users:
- if user.activation_key == "":
+ if user.enabled:
print("Fetching articles for " + user.nickname)
g.user = user
classic_crawler.retrieve_feed(loop, g.user, feed_id)
diff --git a/src/tests/fixtures.py b/src/tests/fixtures.py
index 99f46c37..16a9cb81 100644
--- a/src/tests/fixtures.py
+++ b/src/tests/fixtures.py
@@ -4,7 +4,7 @@ from web.models import db_create, db_empty, User, Article, Feed
def populate_db(db):
role_admin, role_user = db_create(db)
user1, user2 = [User(nickname=name, email="%s@test.te" % name,
- pwdhash=name, roles=[role_user], activation_key="")
+ pwdhash=name, roles=[role_user], enabled=True)
for name in ["user1", "user2"]]
db.session.add(user1)
db.session.add(user2)
diff --git a/src/web/controllers/user.py b/src/web/controllers/user.py
index d8bf1fa1..ae169b05 100644
--- a/src/web/controllers/user.py
+++ b/src/web/controllers/user.py
@@ -9,14 +9,6 @@ class UserController(AbstractController):
_db_cls = User
_user_id_key = 'id'
- def unset_activation_key(self, obj_id):
- self.update({'id': obj_id}, {'activation_key': ""})
-
- def set_activation_key(self, obj_id):
- key = str(random.getrandbits(256)).encode("utf-8")
- key = hashlib.sha512(key).hexdigest()[:86]
- self.update({'id': obj_id}, {'activation_key': key})
-
def _handle_password(self, attrs):
if attrs.get('password'):
attrs['pwdhash'] = generate_password_hash(attrs.pop('password'))
diff --git a/src/web/forms.py b/src/web/forms.py
index 172f31a8..b17d2f7a 100644
--- a/src/web/forms.py
+++ b/src/web/forms.py
@@ -99,10 +99,9 @@ class SigninForm(RedirectForm):
return False
user = User.query.filter(User.email == self.email.data).first()
- if user and user.check_password(self.password.data) \
- and user.activation_key == "":
+ if user and user.check_password(self.password.data) and user.enabled:
return True
- elif user and user.activation_key != "":
+ elif user and not user.enabled:
flash(lazy_gettext('Account not confirmed'), 'danger')
return False
else:
@@ -207,9 +206,9 @@ class RecoverPasswordForm(Form):
return False
user = User.query.filter(User.email == self.email.data).first()
- if user and user.activation_key == "":
+ if user and user.enabled:
return True
- elif user and user.activation_key != "":
+ elif user and not user.enabled:
flash(lazy_gettext('Account not confirmed.'), 'danger')
return False
else:
diff --git a/src/web/lib/user_utils.py b/src/web/lib/user_utils.py
new file mode 100644
index 00000000..78468379
--- /dev/null
+++ b/src/web/lib/user_utils.py
@@ -0,0 +1,23 @@
+
+
+from itsdangerous import URLSafeTimedSerializer
+
+from bootstrap import application
+
+
+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:
+ return False
+ return email
diff --git a/src/web/models/__init__.py b/src/web/models/__init__.py
index e6615ab4..d9489dbb 100644
--- a/src/web/models/__init__.py
+++ b/src/web/models/__init__.py
@@ -96,7 +96,7 @@ def db_create(db):
"root@jarr.localhost"),
pwdhash=generate_password_hash(
os.environ.get("ADMIN_PASSWORD", "password")),
- activation_key="")
+ enabled=True)
user1.roles.extend([role_admin, role_user])
db.session.add(user1)
diff --git a/src/web/models/user.py b/src/web/models/user.py
index cdbfb457..1a276f7e 100644
--- a/src/web/models/user.py
+++ b/src/web/models/user.py
@@ -45,8 +45,7 @@ class User(db.Model, UserMixin):
email = db.Column(db.String(254), index=True, unique=True)
pwdhash = db.Column(db.String())
roles = db.relationship('Role', backref='user', lazy='dynamic')
- activation_key = db.Column(db.String(128), default=hashlib.sha512(
- str(random.getrandbits(256)).encode("utf-8")).hexdigest()[:86])
+ enabled = db.Column(db.Boolean(), default=False)
date_created = db.Column(db.DateTime(), default=datetime.now)
last_seen = db.Column(db.DateTime(), default=datetime.now)
feeds = db.relationship('Feed', backref='subscriber', lazy='dynamic',
diff --git a/src/web/notifications.py b/src/web/notifications.py
index c0d4fb1c..309da2a3 100644
--- a/src/web/notifications.py
+++ b/src/web/notifications.py
@@ -21,6 +21,7 @@
import conf
from web import emails
+from web.lib.user_utils import generate_confirmation_token
def information_message(subject, plaintext):
@@ -30,7 +31,7 @@ def information_message(subject, plaintext):
from web.models import User
users = User.query.all()
# Only send email for activated accounts.
- user_emails = [user.email for user in users if user.activation_key == ""]
+ user_emails = [user.email for user in users if user.enabled]
# Postmark has a limit of twenty recipients per message in total.
for i in xrange(0, len(user_emails), 19):
emails.send(to=conf.NOTIFICATION_EMAIL,
@@ -41,8 +42,9 @@ def new_account_notification(user):
"""
Account creation notification.
"""
+ token = generate_confirmation_token(user.email)
plaintext = """Hello,\n\nYour account has been created. Click on the following link to confirm it:\n%s\n\nSee you,""" % \
- (conf.PLATFORM_URL + 'user/confirm_account/' + user.activation_key)
+ (conf.PLATFORM_URL + 'user/confirm_account/' + token)
emails.send(to=user.email, bcc=conf.NOTIFICATION_EMAIL,
subject="[jarr] Account creation", plaintext=plaintext)
diff --git a/src/web/templates/admin/dashboard.html b/src/web/templates/admin/dashboard.html
index 22e82349..57b20bb5 100644
--- a/src/web/templates/admin/dashboard.html
+++ b/src/web/templates/admin/dashboard.html
@@ -18,7 +18,7 @@
</thead>
<tbody>
{% for user in users|sort(attribute="last_seen")|reverse %}
- <tr {% if user.activation_key != "" %}class="warning"{% endif %}>
+ <tr {% if not user.enabled %}class="warning"{% endif %}>
<td>{{ loop.index }}</td>
<td>{{ user.nickname }}{% if user.id == current_user.id %} (It's you!){% endif %}</td>
<td><a href="mailto:{{ user.email }}">{{ user.email }}</a></td>
@@ -28,7 +28,7 @@
<a href="{{ url_for("admin.user_form", user_id=user.id) }}"><i class="glyphicon glyphicon-edit" title="{{ _('Edit this user') }}"></i></a>
{% if user.id != current_user.id %}
<a href="{{ url_for("admin.toggle_user", user_id=user.id) }}">
- {% if user.activation_key == "" %}
+ {% if user.enabled %}
<i class="glyphicon glyphicon-ban-circle" title="{{ _("Disable this account") }}"></i>
{% else %}
<i class="glyphicon glyphicon-ok-circle" title="{{ _("Enable this account") }}"></i>
diff --git a/src/web/views/admin.py b/src/web/views/admin.py
index 30758f63..832c134d 100644
--- a/src/web/views/admin.py
+++ b/src/web/views/admin.py
@@ -83,13 +83,13 @@ def process_user_form(user_id=None):
flash(gettext('User %(nick)s successfully updated',
nick=user.nickname), 'success')
else:
- # Create a new user
+ # Create a new user (by the admin)
user = user_contr.create(nickname=form.nickname.data,
email=form.email.data,
password=form.password.data,
roles=[role_user],
refresh_rate=form.refresh_rate.data,
- activation_key="")
+ enabled=True)
flash(gettext('User %(nick)s successfully created',
nick=user.nickname), 'success')
return redirect(url_for('admin.user_form', user_id=user.id))
@@ -144,12 +144,11 @@ def toggle_user(user_id=None):
flash(gettext('This user does not exist.'), 'danger')
return redirect(url_for('admin.dashboard'))
- if user.activation_key != "":
-
+ if not user.enabled:
# Send the confirmation email
try:
notifications.new_account_activation(user)
- user_contr.unset_activation_key(user.id)
+ user_contr.update({'id': user.id}, {'enabled': True})
message = gettext('Account of the user %(nick)s successfully '
'activated.', nick=user.nickname)
except Exception as error:
@@ -158,7 +157,7 @@ def toggle_user(user_id=None):
return redirect(url_for('admin.dashboard'))
else:
- user_contr.set_activation_key(user.id)
+ user_contr.update({'id': user.id}, {'enabled': False})
message = gettext('Account of the user %(nick)s successfully disabled',
nick=user.nickname)
flash(message, 'success')
diff --git a/src/web/views/api/common.py b/src/web/views/api/common.py
index 3476cad9..c155a254 100644
--- a/src/web/views/api/common.py
+++ b/src/web/views/api/common.py
@@ -54,8 +54,7 @@ def authenticate(func):
if auth is not None:
user = User.query.filter(
User.nickname == auth.username).first()
- if user and user.check_password(auth.password) \
- and user.activation_key == "":
+ if user and user.check_password(auth.password) and user.enabled:
g.user = user
logged_in = True
if logged_in:
diff --git a/src/web/views/user.py b/src/web/views/user.py
index 754d3b9a..0f9fe612 100644
--- a/src/web/views/user.py
+++ b/src/web/views/user.py
@@ -7,6 +7,7 @@ from flask.ext.login import login_required
import conf
from web import utils, notifications
+from web.lib.user_utils import confirm_token
from web.controllers import (UserController, FeedController, ArticleController)
from web.forms import ProfileForm, RecoverPasswordForm
@@ -102,16 +103,17 @@ def delete_account():
return redirect(url_for('login'))
-@user_bp.route('/confirm_account/<string:activation_key>', methods=['GET'])
-def confirm_account(activation_key=None):
+@user_bp.route('/confirm_account/<string:token>', methods=['GET'])
+def confirm_account(token=None):
"""
Confirm the account of a user.
"""
user_contr = UserController()
- if activation_key != "":
- user = user_contr.read(activation_key=activation_key).first()
+ if token != "":
+ email = confirm_token(token, expiration=3600)
+ user = user_contr.read(email=email).first()
if user is not None:
- user_contr.update({'id': user.id}, {'activation_key': ''})
+ user_contr.update({'id': user.id}, {'enabled': True})
flash(gettext('Your account has been confirmed.'), 'success')
else:
flash(gettext('Impossible to confirm this account.'), 'danger')
bgstack15