diff --git a/CHANGES.rst b/CHANGES.rst index b8a2bf0b..f7f49e6f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ -Change Log -========== +Change history +============== * v1.0 -- Initial version. diff --git a/README.rst b/README.rst index 3ce51753..08c9b3c3 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Secure and Reliable Fully customizable, yet Ready to use ------------------------------------ * **Largely configurable** -- Through configuration settings -* **Fully customizable** -- Through customizable functions and email templates +* **Almost fully customizable** -- Through customizable functions and email templates * **Ready to use** -- Through sensible defaults * Supports **SQL Databases** and **MongoDB Databases** * **Event hooking** -- Through signals diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 00000000..33d5127a --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,6 @@ +/* Switch off word hyphenation */ +div.body p, +div.body dd, +div.body li { + hyphens: None; +} \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index 68aea1ba..2903c4b2 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -73,7 +73,7 @@ Typical use: # Customize the DB Adapter for SQLAlchemy with this User model self.db_adapter = SQLAlchemyAdapter(db, User) # Customize Flask-User settings - self.enable_email = True + self.USER_ENABLE_EMAIL = True # Setup Flask-User user_manager = CustomUserManager(app) @@ -90,7 +90,7 @@ As an a alternative, user_manager.init_app(app) can be used:: # Customize the DB Adapter for SQLAlchemy with this User model self.db_adapter = SQLAlchemyAdapter(db, User) # Customize Flask-User settings - self.enable_email = True + self.USER_ENABLE_EMAIL = True db = SQLAlchemy(app) # Setup SQLAlchemy user_manager = CustomUserManager(UserManager) # Setup Flask-User diff --git a/docs/source/authorization.rst b/docs/source/authorization.rst index 095b7fd3..acbc706a 100644 --- a/docs/source/authorization.rst +++ b/docs/source/authorization.rst @@ -41,7 +41,7 @@ In the example below the current user is required to have the 'admin' role:: Note: Comparison of role names is case sensitive, so 'Member' will NOT match 'member'. Multiple string arguments -- the AND operation -~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The @roles_required decorator accepts multiple strings if the current_user is required to have **ALL** of these roles. @@ -54,7 +54,7 @@ In the example below the current user is required to have the **ALL** of these r Multiple string arguments represent the 'AND' operation. Array arguments -- the OR operation -~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The @roles_required decorator accepts an array (or a tuple) of roles. @@ -65,7 +65,7 @@ In the example below the current user is required to have **One or more** of the # Array arguments require at least ONE of these roles. AND/OR operations -~~~~~~~~ +~~~~~~~~~~~~~~~~~ The potentially confusing syntax described above allows us to construct complex AND/OR operations. @@ -83,7 +83,7 @@ Note: The nesting level only goes as deep as this example shows. Required Tables --------------- +--------------- For @login_required only the User model is required diff --git a/docs/source/conf.py b/docs/source/conf.py index 5c799ba9..95c1cb8d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -187,6 +187,10 @@ autodoc_member_order = 'bysource' autoclass_content = 'both' # Show class doc, but not __init__ doc +def setup(app): + # Disable word hyphenation by HTML/CSS + app.add_stylesheet('custom.css') + # -- Global substitutions rst_epilog = """ .. |supported_python_versions_and| replace:: {supported_python_versions_and} diff --git a/docs/source/configure.rst b/docs/source/configure.rst new file mode 100644 index 00000000..f912616b --- /dev/null +++ b/docs/source/configure.rst @@ -0,0 +1,32 @@ +Configure +========= + +Flask-User is designed to be **largely configurable** and **almost fully customizable**. + +Flask-User default features and settings can overridden in one of two ways: + +1) By changing the settings in the application config file:: + + # Customize Flask-User settings + USER_ENABLE_EMAIL = True + USER_ENABLE_USERNAME = False + +2) By changing the setting in the ``UserManager.customize()``:: + + # Customize Flask-User + class CustomUserManager(UserManager): + + def customize(self): + # Customize Flask-User settings + self.USER_ENABLE_EMAIL = True + self.USER_ENABLE_USERNAME = False + +The :ref:`UserManager` documents all Flask-User settings (over 70 of them). + +If a setting is defined in both the application config file and in ``UserManager.customize()``, +the ``UserManager.customize()`` setting will override the config file setting. + +To keep the code base simple and robust, we offer no easy way to change +the '/user' base URL or the '/flask_user' base directory in bulk. +Please copy them from the :ref:`UserManager` docs use your editor to find-and-replace these bases. + diff --git a/docs/source/customization.rst b/docs/source/customize.rst similarity index 99% rename from docs/source/customization.rst rename to docs/source/customize.rst index d97eeb6c..049ee88e 100644 --- a/docs/source/customization.rst +++ b/docs/source/customize.rst @@ -155,7 +155,7 @@ and define the confirmation specific messages in ``templates/flask_user/emails/c The email template files, along with available template variables listed below: * Template variables available in any email template - * ``user_manager`` - For example: ``{% if user_manager.enable_confirm_email %}`` + * ``user_manager`` - For example: ``{% if user_manager.USER_ENABLE_CONFIRM_EMAIL %}`` * ``user`` - For example: ``{{ user.email }}`` * templates/flask_user/confirm_email_[subject.txt|message.html|message.txt] * ``confirm_email_link`` - For example: ``{{ confirm_email_link }}`` diff --git a/docs/source/index.rst b/docs/source/index.rst index d39350cc..3ffbc473 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,15 +18,16 @@ Table of Contents limitations data_models porting - changes + configure + customize flask_user_api authorization roles_required_app base_templates - customization signals recipes internationalization faq + changes diff --git a/docs/source/misc_api.rst b/docs/source/misc_api.rst index 15969ec4..3ce2dd15 100644 --- a/docs/source/misc_api.rst +++ b/docs/source/misc_api.rst @@ -4,7 +4,8 @@ Miscellaneous API - DbAdapterForSQLAlchemy_ - DbAdapterForMongoAlchemy_ - EmailMailerForFlaskMail_ -- SendmailEmailMailer_ +- EmailMailerForFlaskSendmail_ +- EmailMailerForSendgrid_ -------- diff --git a/docs/source/porting.rst b/docs/source/porting.rst index f328f521..2a751f8f 100644 --- a/docs/source/porting.rst +++ b/docs/source/porting.rst @@ -42,15 +42,17 @@ appropriate DbAdapter will be configured internally. Configuration settings changes ------------------------------ -we renamed the `PASSWORD_HASH` setting to `PASSWORD_HASH_SCHEME` to better reflect what this setting means. +We split ``USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST`` into ``USER_SHOW_USERNAME_DOES_NOT_EXIST`` +and ``USER_SHOW_EMAIL_DOES_NOT_EXIST`` and set the default to False for increased security. Flask-User v0.6:: - USER_PASSWORD_HASH = 'brcypt' + USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST = True Flask-User v1.0:: - USER_PASSWORD_HASH_SCHEME = 'brcypt' + USER_SHOW_EMAIL_DOES_NOT_EXIST = False + USER_SHOW_USERNAME_DOES_NOT_EXIST = False Data-model changes @@ -105,7 +107,7 @@ We changed the `verify_user_password()` parameter order to be consistent with th We renamed `update_password()` to `update_user_hashed_password()` to better reflect what this method does. -we renamed the `PASSWORD_HASH` setting to `PASSWORD_HASH_SCHEME` to better reflect what this setting means. +we renamed the `PASSWORD_HASH` setting to `PASSWORD_HASH` to better reflect what this setting means. Flask-User v0.6:: diff --git a/flask_user/email_manager.py b/flask_user/email_manager.py index ec58a9fd..d66a41bb 100644 --- a/flask_user/email_manager.py +++ b/flask_user/email_manager.py @@ -27,8 +27,8 @@ def send_email_confirmation_email(self, user, user_email): """Send the 'email confirmation' email.""" # Verify email settings - if not self.user_manager.enable_email: return - if not self.user_manager.send_registered_email and not self.user_manager.enable_confirm_email: return + if not self.user_manager.USER_ENABLE_EMAIL: return + if not self.user_manager.USER_SEND_REGISTERED_EMAIL and not self.user_manager.USER_ENABLE_CONFIRM_EMAIL: return # Generate confirm email link object_id = user_email.id if user_email else user.id @@ -41,9 +41,9 @@ def send_email_confirmation_email(self, user, user_email): # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.confirm_email_email_template, + self.user_manager.USER_CONFIRM_EMAIL_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name, + app_name=self.user_manager.USER_APP_NAME, confirm_email_link=confirm_email_link) # Send email message using Flask-Mail @@ -53,8 +53,8 @@ def send_password_has_changed_email(self, user): """Send the 'password has changed' notification email.""" # Verify email settings - if not self.user_manager.enable_email: return - if not self.user_manager.send_password_changed_email: return + if not self.user_manager.USER_ENABLE_EMAIL: return + if not self.user_manager.USER_SEND_PASSWORD_CHANGED_EMAIL: return # Retrieve email address from User or UserEmail object user_email = self.get_primary_user_email(user) @@ -64,9 +64,9 @@ def send_password_has_changed_email(self, user): # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.password_changed_email_template, + self.user_manager.USER_PASSWORD_CHANGED_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name) + app_name=self.user_manager.USER_APP_NAME) # Send email message using Flask-Mail self._send_email_message(email, subject, html_message, text_message) @@ -75,8 +75,8 @@ def send_reset_password_email(self, user, user_email): """Send the 'reset password' email.""" # Verify email settings - if not self.user_manager.enable_email: return - assert self.user_manager.enable_forgot_password + if not self.user_manager.USER_ENABLE_EMAIL: return + assert self.user_manager.USER_ENABLE_FORGOT_PASSWORD # Generate reset password link token = self.user_manager.token_manager.generate_token(user.id) @@ -88,9 +88,9 @@ def send_reset_password_email(self, user, user_email): # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.forgot_password_email_template, + self.user_manager.USER_FORGOT_PASSWORD_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name, + app_name=self.user_manager.USER_APP_NAME, reset_password_link=reset_password_link) # Send email message using Flask-Mail @@ -107,9 +107,9 @@ def send_user_invitation_email(self, user): # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.invite_email_template, + self.user_manager.USER_INVITE_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name, + app_name=self.user_manager.USER_APP_NAME, accept_invite_link=accept_invite_link) # Send email message using Flask-Mail @@ -119,8 +119,8 @@ def send_user_has_registered_email(self, user, user_email, confirm_email_link): """Send the 'user has registered' notification email.""" # Verify email settings - if not self.user_manager.enable_email: return - if not self.user_manager.send_registered_email: return + if not self.user_manager.USER_ENABLE_EMAIL: return + if not self.user_manager.USER_SEND_REGISTERED_EMAIL: return # Retrieve email address from User or UserEmail object email = user_email.email if user_email else user.email @@ -128,9 +128,9 @@ def send_user_has_registered_email(self, user, user_email, confirm_email_link): # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.registered_email_template, + self.user_manager.USER_REGISTERED_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name, + app_name=self.user_manager.USER_APP_NAME, confirm_email_link=confirm_email_link) # Send email message using Flask-Mail @@ -140,8 +140,8 @@ def send_username_has_changed_email(self, user): # pragma: no cover """Send the 'username has changed' notification email.""" # Verify email settings - if not self.user_manager.enable_email: return - if not self.user_manager.send_username_changed_email: return + if not self.user_manager.USER_ENABLE_EMAIL: return + if not self.user_manager.USER_SEND_USERNAME_CHANGED_EMAIL: return # Retrieve email address from User or UserEmail object user_email = self.get_primary_user_email(user) @@ -151,9 +151,9 @@ def send_username_has_changed_email(self, user): # pragma: no cover # Render subject, html message and text message subject, html_message, text_message = self._render_email( - self.user_manager.username_changed_email_template, + self.user_manager.USER_USERNAME_CHANGED_EMAIL_TEMPLATE, user=user, - app_name=self.user_manager.app_name) + app_name=self.user_manager.USER_APP_NAME) # Send email message using Flask-Mail self._send_email_message(email, subject, html_message, text_message) diff --git a/flask_user/forms.py b/flask_user/forms.py index d4de1205..d8f5f246 100644 --- a/flask_user/forms.py +++ b/flask_user/forms.py @@ -97,7 +97,7 @@ class ChangePasswordForm(FlaskForm): def validate(self): # Use feature config to remove unused form fields user_manager = current_app.user_manager - if not user_manager.enable_retype_password: + if not user_manager.USER_ENABLE_RETYPE_PASSWORD: delattr(self, 'retype_password') # Add custom password validator if needed @@ -165,7 +165,7 @@ class ForgotPasswordForm(FlaskForm): def validate_email(form, field): user_manager = current_app.user_manager - if user_manager.show_username_email_does_not_exist: + if user_manager.USER_SHOW_EMAIL_DOES_NOT_EXIST: user, user_email = user_manager.find_user_by_email(field.data) if not user: raise ValidationError(_('%(username_or_email)s does not exist', username_or_email=_('Email'))) @@ -192,14 +192,14 @@ class LoginForm(FlaskForm): def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) user_manager = current_app.user_manager - if user_manager.enable_username and user_manager.enable_email: + if user_manager.USER_ENABLE_USERNAME and user_manager.USER_ENABLE_EMAIL: # Renamed 'Username' label to 'Username or Email' self.username.label.text = _('Username or Email') def validate(self): # Remove fields depending on configuration user_manager = current_app.user_manager - if user_manager.enable_username: + if user_manager.USER_ENABLE_USERNAME: delattr(self, 'email') else: delattr(self, 'username') @@ -211,12 +211,12 @@ def validate(self): # Find user by username and/or email user = None user_email = None - if user_manager.enable_username: + if user_manager.USER_ENABLE_USERNAME: # Find user by username user = user_manager.find_user_by_username(self.username.data) # Find user by email address (username field) - if not user and user_manager.enable_email: + if not user and user_manager.USER_ENABLE_EMAIL: user, user_email = user_manager.find_user_by_email(self.username.data) else: @@ -229,25 +229,28 @@ def validate(self): # Handle unsuccessful authentication # Email, Username or Email/Username depending on settings - if user_manager.enable_username and user_manager.enable_email: + if user_manager.USER_ENABLE_USERNAME and user_manager.USER_ENABLE_EMAIL: username_or_email_field = self.username username_or_email_text = (_('Username/Email')) - elif user_manager.enable_username: + show_does_not_exist = user_manager.USER_SHOW_EMAIL_DOES_NOT_EXIST or user_manager.USER_SHOW_USERNAME_DOES_NOT_EXIST + elif user_manager.USER_ENABLE_USERNAME: username_or_email_field = self.username username_or_email_text = (_('Username')) + show_does_not_exist = user_manager.USER_SHOW_USERNAME_DOES_NOT_EXIST else: username_or_email_field = self.email username_or_email_text = (_('Email')) + show_does_not_exist = user_manager.USER_SHOW_EMAIL_DOES_NOT_EXIST - # Show 'username/email does not exist error message - if user_manager.show_username_email_does_not_exist: + # Show 'username/email does not exist' or 'incorrect password' error message + if show_does_not_exist: if not user: message = _('%(username_or_email)s does not exist', username_or_email=username_or_email_text) username_or_email_field.errors.append(message) else: self.password.errors.append(_('Incorrect Password')) - # Hide 'username/email does not exist error message for additional security + # Always show 'incorrect username/email or password' error message for additional security else: message = _('Incorrect %(username_or_email)s and/or Password', username_or_email=username_or_email_text) username_or_email_field.errors.append(message) @@ -280,14 +283,14 @@ class RegisterForm(FlaskForm): def validate(self): # remove certain form fields depending on user manager config user_manager = current_app.user_manager - if not user_manager.enable_username: + if not user_manager.USER_ENABLE_USERNAME: delattr(self, 'username') - if not user_manager.enable_email: + if not user_manager.USER_ENABLE_EMAIL: delattr(self, 'email') - if not user_manager.enable_retype_password: + if not user_manager.USER_ENABLE_RETYPE_PASSWORD: delattr(self, 'retype_password') # Add custom username validator if needed - if user_manager.enable_username: + if user_manager.USER_ENABLE_USERNAME: has_been_added = False for v in self.username.validators: if v==user_manager.username_validator: @@ -327,7 +330,7 @@ class ResetPasswordForm(FlaskForm): def validate(self): # Use feature config to remove unused form fields user_manager = current_app.user_manager - if not user_manager.enable_retype_password: + if not user_manager.USER_ENABLE_RETYPE_PASSWORD: delattr(self, 'retype_password') # Add custom password validator if needed has_been_added = False diff --git a/flask_user/password_manager.py b/flask_user/password_manager.py index 4751038b..d2df2c60 100644 --- a/flask_user/password_manager.py +++ b/flask_user/password_manager.py @@ -7,17 +7,17 @@ from __future__ import print_function -from passlib.context import CryptContext -import hashlib -import hmac -import base64 from flask import current_app +from passlib.context import CryptContext -def generate_sha512_hmac(self, password_salt, password): - """ Generate SHA512 HMAC -- for compatibility with Flask-Security """ - return base64.b64encode(hmac.new(password_salt, password.encode('utf-8'), hashlib.sha512).digest()) +# def generate_sha512_hmac(self, password_salt, password): +# """ Generate SHA512 HMAC -- for compatibility with Flask-Security """ +# import hashlib +# import hmac +# import base64 +# return base64.b64encode(hmac.new(password_salt, password.encode('utf-8'), hashlib.sha512).digest()) # The UserManager is implemented across several source code files. @@ -40,9 +40,9 @@ def __init__(self, password_crypt_context, password_hash_scheme, password_hash_m def hash_password(self, password): """ Generate hashed password using SHA512 HMAC and the USER_PASSWORD_HASH hash function.""" - # Pre-generate SHA512 HMAC -- For compatibility with Flask-Security - if self.password_hash_mode == 'Flask-Security': - password = generate_sha512_hmac(self.password_salt, password) + # # Pre-generate SHA512 HMAC -- For compatibility with Flask-Security + # if self.password_hash_mode == 'Flask-Security': + # password = generate_sha512_hmac(self.password_salt, password) # Use passlib's CryptContext to hash password hashed_password = self.password_crypt_context.encrypt(password) @@ -55,7 +55,7 @@ def verify_user_password(self, user, password): Returns True on matching password. Returns False otherwise.""" - # Perform some Python magic to allow for: + # Perform some Python magic to flip param order for v0.6-style calls: # - v0.6 verify_user_password(password, user), and # - v0.9+ verify_user_password(user, password) parameter order if isinstance(user, (b''.__class__, u''.__class__)): @@ -66,9 +66,9 @@ def verify_user_password(self, user, password): hashed_password = user.password - # Pre-generate SHA512 HMAC -- For compatibility with Flask-Security - if self.password_hash_mode == 'Flask-Security': - password = generate_sha512_hmac(self.password_salt, password) + # # Pre-generate SHA512 HMAC -- For compatibility with Flask-Security + # if self.password_hash_mode == 'Flask-Security': + # password = generate_sha512_hmac(self.password_salt, password) # Use passlib's CryptContext to verify return self.password_crypt_context.verify(password, hashed_password) diff --git a/flask_user/templates/base.html b/flask_user/templates/base.html index b6909de2..85f859e7 100644 --- a/flask_user/templates/base.html +++ b/flask_user/templates/base.html @@ -4,7 +4,7 @@ -
Your password has been changed.
-{% if user_manager.enable_forgot_password %} +{% if user_manager.USER_ENABLE_FORGOT_PASSWORD %}If you did not initiate this password change, click here to reset it.
{% endif %} {% endblock %} diff --git a/flask_user/templates/flask_user/emails/registered_message.html b/flask_user/templates/flask_user/emails/registered_message.html index cc3305c2..dcbe0622 100644 --- a/flask_user/templates/flask_user/emails/registered_message.html +++ b/flask_user/templates/flask_user/emails/registered_message.html @@ -4,7 +4,7 @@Thank you for registering with {{ app_name }}.
-{% if user_manager.enable_confirm_email and not user.email_confirmed_at %} +{% if user_manager.USER_ENABLE_CONFIRM_EMAIL and not user.email_confirmed_at %}You will need to confirm your email next.
If you initiated this registration, please click on the link below:
diff --git a/flask_user/templates/flask_user/login.html b/flask_user/templates/flask_user/login.html
index 3d8f9634..4a62d502 100644
--- a/flask_user/templates/flask_user/login.html
+++ b/flask_user/templates/flask_user/login.html
@@ -8,7 +8,7 @@
@@ -45,16 +45,16 @@ {%trans%}Register{%endtrans%}
{{ register_form.hidden_tag() }}
{# Username or Email #}
- {% set field = register_form.username if user_manager.enable_username else register_form.email %}
+ {% set field = register_form.username if user_manager.USER_ENABLE_USERNAME else register_form.email %}
{{ render_field(field, tabindex=210) }}
- {% if user_manager.enable_email and user_manager.enable_username %}
+ {% if user_manager.USER_ENABLE_EMAIL and user_manager.USER_ENABLE_USERNAME %}
{{ render_field(register_form.email, tabindex=220) }}
{% endif %}
{{ render_field(register_form.password, tabindex=230) }}
- {% if user_manager.enable_retype_password %}
+ {% if user_manager.USER_ENABLE_RETYPE_PASSWORD %}
{{ render_field(register_form.retype_password, tabindex=240) }}
{% endif %}
diff --git a/flask_user/templates/flask_user/register.html b/flask_user/templates/flask_user/register.html
index 0c2c924c..a6ecad42 100644
--- a/flask_user/templates/flask_user/register.html
+++ b/flask_user/templates/flask_user/register.html
@@ -8,7 +8,7 @@ {%trans%}Register{%endtrans%}
{{ form.hidden_tag() }}
{# Username or Email #}
- {% set field = form.username if user_manager.enable_username else form.email %}
+ {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %}
{%trans%}Register{%endtrans%}
{%trans%}Register{%endtrans%}
{% endif %}
{%trans%}Reset Password{%endtrans%}