├── .gitignore ├── .pypirc ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── babel.cfg ├── docs └── source │ ├── _static │ └── custom.css │ ├── _templates │ ├── about.html │ └── globaltoc.html │ ├── _todo.rst │ ├── api.rst │ ├── api_db_adapters.rst │ ├── api_decorators.rst │ ├── api_email_adapters.rst │ ├── api_forms.rst │ ├── api_user_manager.rst │ ├── api_user_manager__settings.rst │ ├── api_user_manager__utils.rst │ ├── api_user_manager__views.rst │ ├── authorization.rst │ ├── base_templates.rst │ ├── basic_app.rst │ ├── changes.rst │ ├── conf.py │ ├── configuring_settings.rst │ ├── customizing.rst │ ├── customizing_advanced.rst │ ├── customizing_emails.rst │ ├── customizing_forms.rst │ ├── data_models.rst │ ├── faq.rst │ ├── includes │ ├── customizing_submenu.rst │ ├── main_page.rst │ ├── porting_submenu.rst │ ├── quickstart_submenu.rst │ ├── signals.txt │ ├── submenu_defs.rst │ ├── template_functions.txt │ └── template_variables.txt │ ├── index.rst │ ├── installation.rst │ ├── internationalization.rst │ ├── introduction.rst │ ├── limitations.rst │ ├── mongodb_app.rst │ ├── old_api.rst │ ├── porting.rst │ ├── porting_advanced.rst │ ├── porting_basics.rst │ ├── porting_customizations.rst │ ├── quickstart.rst │ ├── quickstart_app.rst │ ├── recipes.rst │ ├── signals.rst │ └── unused.rst ├── example_apps ├── __init__.py ├── auth0_app.py ├── basic_app.py ├── dynamodb_app.py ├── invite_app.py ├── mongodb_app.py ├── multi_email_app.py ├── pynamodb_app.py └── quickstart_app.py ├── fabfile.py ├── flask_user ├── README.rst ├── __init__.py ├── db_adapters │ ├── __init__.py │ ├── db_adapter_interface.py │ ├── dynamo_db_adapter.py │ ├── mongo_db_adapter.py │ ├── pynamo_db_adapter.py │ └── sql_db_adapter.py ├── db_manager.py ├── decorators.py ├── email_adapters │ ├── __init__.py │ ├── email_adapter_interface.py │ ├── sendgrid_email_adapter.py │ ├── sendmail_email_adapter.py │ └── smtp_email_adapter.py ├── email_manager.py ├── forms.py ├── legacy_error.py ├── password_manager.py ├── signals.py ├── templates │ ├── flask_user │ │ ├── _authorized_base.html │ │ ├── _common_base.html │ │ ├── _macros.html │ │ ├── _public_base.html │ │ ├── change_password.html │ │ ├── change_username.html │ │ ├── edit_user_profile.html │ │ ├── emails │ │ │ ├── base_message.html │ │ │ ├── base_message.txt │ │ │ ├── base_subject.txt │ │ │ ├── confirm_email_message.html │ │ │ ├── confirm_email_message.txt │ │ │ ├── confirm_email_subject.txt │ │ │ ├── invite_user_message.html │ │ │ ├── invite_user_message.txt │ │ │ ├── invite_user_subject.txt │ │ │ ├── password_changed_message.html │ │ │ ├── password_changed_message.txt │ │ │ ├── password_changed_subject.txt │ │ │ ├── registered_message.html │ │ │ ├── registered_message.txt │ │ │ ├── registered_subject.txt │ │ │ ├── reset_password_message.html │ │ │ ├── reset_password_message.txt │ │ │ ├── reset_password_subject.txt │ │ │ ├── username_changed_message.html │ │ │ ├── username_changed_message.txt │ │ │ └── username_changed_subject.txt │ │ ├── forgot_password.html │ │ ├── invite_user.html │ │ ├── login.html │ │ ├── login_auth0.html │ │ ├── login_or_register.html │ │ ├── manage_emails.html │ │ ├── register.html │ │ ├── resend_confirm_email.html │ │ └── reset_password.html │ └── flask_user_layout.html ├── tests │ ├── .coveragerc │ ├── __init__.py │ ├── conftest.py │ ├── test_invalid_forms.py │ ├── test_misc.py │ ├── test_roles.py │ ├── test_views.py │ ├── test_zzz_db_adapters.py │ ├── tst_app.py │ ├── tst_utils.py │ └── utils.py ├── token_manager.py ├── translation_utils.py ├── translations │ ├── babel.cfg │ ├── de │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── en │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── es │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── fa │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── fi │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── flask_user.pot │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── it │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── nl │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── ru │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── sk │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── sv │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── tr │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ └── flask_user.po │ └── zh │ │ └── LC_MESSAGES │ │ └── flask_user.po ├── user_manager.py ├── user_manager__settings.py ├── user_manager__utils.py ├── user_manager__views.py └── user_mixin.py ├── requirements.txt ├── runserver.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Standard Python.gitignore 2 | # ------------------------- 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | 104 | # Other standard items 105 | # -------------------- 106 | 107 | # Mac OS 108 | .DS_Store 109 | 110 | # IDEs 111 | .project 112 | .pydevproject 113 | .idea 114 | .vscode 115 | 116 | # Profiles 117 | prof/ 118 | 119 | 120 | # Project-specific items 121 | ------------------------ 122 | 123 | # Temp files 124 | *.sqlite 125 | *.sqlite-journal 126 | example_apps/*.sqlite 127 | example_apps/*.sqlite-journal 128 | example_apps/translations/ 129 | 130 | # Private files 131 | local_settings.py 132 | example_apps/local_settings.py 133 | -------------------------------------------------------------------------------- /.pypirc: -------------------------------------------------------------------------------- 1 | [distutils] 2 | index-servers=pypi 3 | 4 | [pypi] 5 | username = lingthio 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis config file 2 | # ================== 3 | 4 | language: python 5 | 6 | # Supported Python versions. 7 | # Please keep this list in sync with `tox.ini`. 8 | python: 9 | - "2.7" 10 | - "3.4" 11 | - "3.5" 12 | - "3.6" 13 | - "3.7" 14 | - "3.8" 15 | 16 | services: mongodb 17 | 18 | # Install test dependencies 19 | before_install: 20 | - python --version 21 | - pip install pytest pytest-cov 22 | - pip install codecov 23 | 24 | # Install project depencies 25 | install: 26 | - pip install -r requirements.txt --quiet 27 | 28 | # Run automated tests 29 | script: 30 | - pytest --cov flask_user --cov-report term-missing --cov-config flask_user/tests/.coveragerc flask_user/tests 31 | 32 | # Submit coverage report to coveralls 33 | after_success: 34 | - codecov 35 | 36 | # Upload a package to PyPI when new tag appears 37 | deploy: 38 | provider: pypi 39 | skip_existing: true 40 | user: "__token__" 41 | on: 42 | tags: true 43 | repo: lingthio/Flask-User 44 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | | **Lead developer and Maintainer** 4 | | Ling Thio -- ling.thio AT gmail DOT com 5 | | 6 | | **Contributors** 7 | | `Many contributors `_ 8 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Change history 2 | ============== 3 | 4 | With **v1.0** we simplified customization 5 | by allowing developers to override or extend ``UserManager`` properties and methods. 6 | 7 | We increased security by having the TokenManager accept parts of passwords, 8 | in addition to the user ID, to invalidate tokens after a password has changed. 9 | The TokenManager now also excepts IDs other than small integers. 10 | 11 | Hashlib password hashing is completely configurable through two config settings: 12 | ``USER_PASSLIB_CRYPTCONTEXT_SCHEMES`` and ``USER_PASSLIB_CRYPTCONTEXT_KEYWORDS``. 13 | Example: ``SCHEMES=['bcrypt', 'argon2']``, ``KEYWORDS=dict(bcrypt__rounds=12, argon2__memory_cost=512)``. 14 | 15 | We added support for MongoDBs (through Flask-MongoEngine) 16 | and for DynamoDBs (through Flask-Flywheel). 17 | 18 | We introduced the EmailAdapter interface to support sending emails not only via SMTP, 19 | but also via ``sendmail``, SendGrid, and custom EmailAdapters. 20 | 21 | For all of the above we finally had to break compatibility with **v0.6 (stable)**. 22 | For non-customized Flask-User apps, the porting is relatively straightforward. 23 | See the 'Porting from v0.6 to v1.0+' section in our docs. 24 | 25 | * v1.0.2.3: 26 | * Display "flash" message when change password fails 27 | (see `#239 `_). 28 | * v1.0.2.2: 29 | * Added new settings to ``UserManager`` which can be used to customize page 30 | footers: ``USER_APP_VERSION``, ``USER_CORPORATION_NAME``, and 31 | ``USER_COPYRIGHT_YEAR`` (see `#280 `_). 32 | * Fixed crash when one tried to change username and ``USER_ENABLE_EMAIL`` 33 | was falsy (see `#267 `_). 34 | * v1.0.2.1: 35 | * Added Slovak, Polish and Ukrainian translations. 36 | * Fixed bug in "Password Changed" email template (see `#250 `_). 37 | * Fixed crash when USER_ENABLE_INVITE_USER is set (see `#223 `_). 38 | * Updated min allowed version of ``passlib`` from 1.6 to 1.7 (see `#266 `_). 39 | * v1.0.2.0 - Production/Stable release. Dropped support for Python 2.6 and 3.3. 40 | * v1.0.1.5 - Removed callbacks/auth0. 41 | * v1.0.1.4 - Fixed calls to get_primary_user_email_object(). 42 | * v1.0.1.3 - Changed custom form class attribute names from something like ``self.register_form`` to something like ``self.RegisterFormClass`` 43 | * v1.0.1.2 - Use app.permanent_session_lifetime to limit user session lifetime. 44 | * v1.0.1.1 - Alpha release. Breaks backward compatibility with v0.6. 45 | 46 | * v0.6.* - Previous version. No longer supported. 47 | * v0.5.* - Previous version. No longer supported. 48 | 49 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ling Thio 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst CHANGES.rst LICENSE.txt README.rst 2 | recursive-include flask_user *.py *.html *.txt *.cfg *.pot *.mo *.po 3 | 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-User v1.0 2 | =============== 3 | 4 | .. image:: https://travis-ci.org/lingthio/Flask-User.svg?branch=master 5 | :target: https://travis-ci.org/lingthio/Flask-User 6 | 7 | .. image:: https://codecov.io/gh/lingthio/Flask-User/branch/master/graph/badge.svg 8 | :target: https://codecov.io/gh/lingthio/Flask-User 9 | 10 | .. image:: https://img.shields.io/pypi/v/Flask-User.svg 11 | :target: https://pypi.org/project/Flask-User 12 | 13 | .. image:: https://img.shields.io/pypi/pyversions/Flask-User.svg 14 | :target: https://pypi.org/project/Flask-User 15 | 16 | .. image:: https://img.shields.io/pypi/l/Flask-User?style=flat 17 | :target: https://pypi.org/project/Flask-User 18 | 19 | | **Attention:** 20 | | Flask-User v1.0 is a Production/Stable version. 21 | | The previous version is `Flask-User v0.6 `_. 22 | 23 | 24 | User Authentication and Management 25 | ---------------------------------- 26 | | So, you're writing a Flask web application and would like to authenticate your users. 27 | | You start with a simple **Login** page, but soon enough you'll need to handle: 28 | 29 | * **Registrations** and **Email Confirmations** 30 | * **Change Usernames**, **Change Passwords**, and **Forgotten Passwords** 31 | 32 | And wouldn't it be nice to also offer: 33 | 34 | * **Added Security** 35 | * **Increased Reliability** 36 | * **Role-based Authorization** 37 | * **Internationalization** (Chinese, Dutch, English, Farsi, Finnish, French, German, Italian, Polish, Russian, Slovak, Spanish, Swedish, Turkish and Ukrainian) 38 | 39 | 40 | Customizable, yet Ready to use 41 | ------------------------------ 42 | * **Largely Configurable** -- By overriding configuration settings. 43 | * **Fully Customizable** -- By overriding methods and properties. 44 | * **Ready to use** -- Through sensible defaults. 45 | * Supports **SQL** and **MongoDB** databases. 46 | 47 | 48 | Well documented 49 | --------------- 50 | - `Latest documentation `_ 51 | - `Flask-User v0.6 documentation `_ 52 | - `Flask-User v0.5 documentation `_ 53 | 54 | Additional features 55 | ------------------- 56 | * **MIT License** 57 | * **Tested** on Python 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. Coverage: Over 90%. 58 | * **Event hooking** -- Through efficient signals. 59 | * **Support for multiple emails per user** 60 | 61 | Minimal Requirements 62 | -------------------- 63 | - bcrypt 2.0+ 64 | - cryptography 1.6+ 65 | - Flask 0.9+ 66 | - Flask-Login 0.2+ 67 | - Flask-WTF 0.9+ 68 | - passlib 1.7+ 69 | 70 | Alternatives 71 | ------------ 72 | * `Flask-Login `_ 73 | * `Flask-Security `_ 74 | 75 | Authors 76 | ------- 77 | | **Lead developer and Maintainer** 78 | | Ling Thio -- https://github.com/lingthio 79 | | 80 | | **Collaborators and Maintainers** 81 | | Andrey Semakin -- https://github.com/and-semakin 82 | | 83 | | **Contributors** 84 | | `Many contributors `_ 85 | 86 | Contact us 87 | ---------- 88 | .. image:: http://img.shields.io/static/v1?label=Issues&message=Flask-User&logo=github 89 | :target: https://github.com/lingthio/Flask-User/issues 90 | 91 | .. image:: https://badges.gitter.im/Flask-User/community.svg 92 | :target: https://gitter.im/Flask-User/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge 93 | 94 | .. image:: http://img.shields.io/static/v1?label=Telegram&message=@flask_user&logo=telegram&color=blue 95 | :target: https://t.me/flask_user 96 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: flask_user/**.py] 2 | [jinja2: flask_user/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ -------------------------------------------------------------------------------- /docs/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* Switch off word hyphenation */ 2 | div.body p, 3 | div.body dd, 4 | div.body li { 5 | hyphens: None; 6 | } 7 | 8 | /* Reduce paragraph spacing */ 9 | p { 10 | -webkit-margin-before: 0.8em; 11 | -webkit-margin-after: 0.8em; 12 | } 13 | 14 | /* Format code-blocks */ 15 | div.highlight pre { 16 | margin: 0.5em 0px; 17 | padding: 0.7em 1em; 18 | font-size: 0.8em; 19 | } 20 | 21 | /* Format line numbers (left of code-blocks) */ 22 | td.linenos pre { 23 | font-size: 0.8em; 24 | } 25 | 26 | /* Reduce margins for notes and warnings */ 27 | div.admonition { 28 | margin: 0.5em 0px; 29 | padding: 0.5em 1em; 30 | font-size: 1.0em; 31 | } 32 | -------------------------------------------------------------------------------- /docs/source/_templates/about.html: -------------------------------------------------------------------------------- 1 | 2 |

Flask-User v1.0

3 |
4 | Customizable
5 | User Authentication
6 | & Management
7 |
8 | -------------------------------------------------------------------------------- /docs/source/_templates/globaltoc.html: -------------------------------------------------------------------------------- 1 |

{{ _('Table Of Contents') }}

2 | {{ toctree(maxdepth=1) }} -------------------------------------------------------------------------------- /docs/source/_todo.rst: -------------------------------------------------------------------------------- 1 | - Consider replacing @allow_unconfirmed_email with 2 | @login_required_allow_unconfirmed_email, 3 | @roles_accepted_allow_unconfirmed_email, and 4 | @roles_required_allow_unconfirmed_email 5 | Pros: No need for global setting g._flask_user_allow_unconfirmed_email 6 | Cons: Three extra decorators needed 7 | Decorator X could test for email and then call X_allow_unconfirmed_email 8 | 9 | - Increase test coverage 10 | - 45 lines in user_manager_views: invite_user_view 11 | - 6 lines in user_manager_views: if invite_token and um.UserInvitationClass: 12 | - 4 lines Test with UserEmailClass 13 | - 2 lines in user_manager__utils: confirm_email_view with invalid token 14 | - 3 lines in forms: Test with USER_SHOW_EMAIL_DOES_NOT_EXIST 15 | - 3 lines in user_manager__views: edit_user_profile_view 16 | 17 | - For autodocs of interfaces, init params are show twice 18 | - Idea: 19 | - conf.py: remove autoclass_content = 'both' 20 | - All classes except interfaces: Move __init__ docstring to class docstring 21 | 22 | - API docs: 23 | Submenu1: Intro, UserManager, Decorators, Forms, Views, Interfaces 24 | UserManager submenu2: UserManager, Settings, Utils, Views 25 | Why two views? 26 | Move Managers to Intefaces 27 | 28 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | Flask-User API 2 | ============== 3 | 4 | - :ref:`UserManagerClass` 5 | - :ref:`UserManager__Settings` 6 | - :ref:`UserManager__Utils` 7 | - :ref:`UserManager__Views` 8 | - :ref:`ViewDecorators` 9 | - :ref:`FlaskUserForms` 10 | - :ref:`DBManager` 11 | - :ref:`EMailManager` 12 | - :ref:`PasswordManager` 13 | - :ref:`TokenManager` 14 | - :ref:`DbAdapterInterface` 15 | - :ref:`EmailAdapterInterface` 16 | 17 | -------- 18 | 19 | .. _DBManager: 20 | 21 | DBManager class 22 | --------------- 23 | 24 | This class manages database objects. 25 | 26 | .. autoclass:: flask_user.db_manager.DBManager 27 | 28 | -------- 29 | 30 | .. _EmailManager: 31 | 32 | EmailManager class 33 | ------------------ 34 | 35 | This class manages the sending of Flask-User emails. 36 | 37 | .. autoclass:: flask_user.email_manager.EmailManager 38 | 39 | .. seealso:: :ref:`Customizing the EmailManager `. 40 | 41 | -------- 42 | 43 | .. _PasswordManager: 44 | 45 | PasswordManager class 46 | --------------------- 47 | 48 | The PasswordManager generates and verifies hashed passwords. 49 | 50 | .. autoclass:: flask_user.password_manager.PasswordManager 51 | :no-undoc-members: 52 | 53 | .. seealso:: :ref:`Customizing the PasswordManager `. 54 | 55 | -------- 56 | 57 | .. _TokenManager: 58 | 59 | TokenManager class 60 | ------------------ 61 | 62 | The TokenManager generates and verifies timestamped, signed and encrypted tokens. 63 | 64 | These tokens are used in the following places: 65 | 66 | * To securely store User IDs in the browser session cookie. 67 | * To provide secure tokens in email-confirmation emails. 68 | * To provide secure tokens in reset-password emails. 69 | 70 | .. autoclass:: flask_user.token_manager.TokenManager 71 | :no-undoc-members: 72 | 73 | .. seealso:: :ref:`Customizing the TokenManager `. 74 | -------------------------------------------------------------------------------- /docs/source/api_db_adapters.rst: -------------------------------------------------------------------------------- 1 | .. _DbAdapterInterface: 2 | 3 | DbAdapter Interface 4 | =================== 5 | 6 | The DbAdapterInterface class defines an interface to find, add, update and remove 7 | persistent database objects, 8 | while shielding the Flask-User code from the underlying implementation. 9 | 10 | .. autoclass:: flask_user.db_adapters.db_adapter_interface.DbAdapterInterface 11 | :special-members: __init__ 12 | 13 | .. _CustomDbAdapters: 14 | 15 | Implementing a CustomDbAdapter 16 | ------------------------------ 17 | You can write you own DbAdapter implementation by defining a CustomDbAdapter class 18 | and configure Flask-User to use this class like so:: 19 | 20 | # Define a CustomDbAdapter 21 | from flask_user.db_adapters import DbAdapterInterface 22 | class CustomDbAdapter(DbAdapterInterface): 23 | ... 24 | 25 | # Setup Flask-User 26 | user_manager = UserManager(app, db, User) 27 | 28 | # Customize Flask-User 29 | user_manager.db_adapter = CustomDbAdapter(app) 30 | 31 | For an example, see `the SQLDbAdapter() implementation `_. 32 | -------------------------------------------------------------------------------- /docs/source/api_decorators.rst: -------------------------------------------------------------------------------- 1 | .. _ViewDecorators: 2 | 3 | View decorators 4 | =============== 5 | Flask-User view decorators serve as the gatekeepers to prevent 6 | unauthenticated or unauthorized users from accessing certain views. 7 | 8 | .. important:: 9 | 10 | The @route decorator must always be 11 | the **first** view decorator in a list of view decorators 12 | (because it's used to map the function *below itself* to a URL). 13 | 14 | .. autofunction:: flask_user.decorators.login_required 15 | .. autofunction:: flask_user.decorators.roles_accepted 16 | .. autofunction:: flask_user.decorators.roles_required 17 | .. autofunction:: flask_user.decorators.allow_unconfirmed_email 18 | -------------------------------------------------------------------------------- /docs/source/api_email_adapters.rst: -------------------------------------------------------------------------------- 1 | .. _EmailAdapterInterface: 2 | 3 | EmailAdapter Interface 4 | ====================== 5 | 6 | The EmailAdapterInterface class defines an interface to send email messages 7 | while shielding the Flask-User code from the underlying implementation. 8 | 9 | .. autoclass:: flask_user.email_adapters.email_adapter_interface.EmailAdapterInterface 10 | :special-members: __init__ 11 | 12 | .. tip:: 13 | 14 | :: 15 | 16 | def __init__(self, app): 17 | self.app = app 18 | self.sender_name = self.app.user_manager.USER_EMAIL_SENDER_NAME 19 | self.sender_email = self.app.user_manager.USER_EMAIL_SENDER_EMAIL 20 | 21 | def send_email_message(...): 22 | # use self.sender_name and self.sender_email here... 23 | 24 | .. _CustomEmailAdapters: 25 | 26 | Implementing a CustomEmailAdapter 27 | --------------------------------- 28 | You can write you own EmailAdapter implementation by defining a CustomEmailAdapter class 29 | and configure Flask-User to use this class like so:: 30 | 31 | # Define a CustomEmailAdapter 32 | from flask_user.email_adapters import EmailAdapterInterface 33 | class CustomEmailAdapter(EmailAdapterInterface): 34 | ... 35 | 36 | # Setup Flask-User 37 | user_manager = UserManager(app, db, User) 38 | 39 | # Customize Flask-User 40 | user_manager.email_adapter = CustomDbAdapter(app) 41 | 42 | For an example, see `the SMTPEmailAdapter() implementation `_. 43 | -------------------------------------------------------------------------------- /docs/source/api_forms.rst: -------------------------------------------------------------------------------- 1 | .. _FlaskUserForms: 2 | 3 | Forms 4 | ===== 5 | 6 | Below is a complete list of customizable Flask-User forms. 7 | 8 | .. _AddEmailForm: 9 | 10 | AddEmailForm 11 | ------------ 12 | 13 | .. autoclass:: flask_user.forms.AddEmailForm 14 | :no-undoc-members: 15 | :no-inherited-members: 16 | 17 | -------- 18 | 19 | .. _ChangeUsernameForm: 20 | 21 | ChangeUsernameForm 22 | ------------------ 23 | 24 | .. autoclass:: flask_user.forms.ChangeUsernameForm 25 | :no-undoc-members: 26 | :no-inherited-members: 27 | 28 | -------- 29 | 30 | .. _EditUserProfileForm: 31 | 32 | EditUserProfileForm 33 | ------------------- 34 | 35 | .. autoclass:: flask_user.forms.EditUserProfileForm 36 | :no-undoc-members: 37 | :no-inherited-members: 38 | 39 | -------- 40 | 41 | .. _ForgotPasswordForm: 42 | 43 | ForgotPasswordForm 44 | ------------------ 45 | 46 | .. autoclass:: flask_user.forms.ForgotPasswordForm 47 | :no-undoc-members: 48 | :no-inherited-members: 49 | 50 | -------- 51 | 52 | .. _InviteUserForm: 53 | 54 | InviteUserForm 55 | -------------- 56 | 57 | .. autoclass:: flask_user.forms.InviteUserForm 58 | :no-undoc-members: 59 | :no-inherited-members: 60 | 61 | -------- 62 | 63 | .. _LoginForm: 64 | 65 | LoginForm 66 | --------- 67 | 68 | .. autoclass:: flask_user.forms.LoginForm 69 | :no-undoc-members: 70 | :no-inherited-members: 71 | 72 | -------- 73 | 74 | .. _RegisterForm: 75 | 76 | RegisterForm 77 | ---------------- 78 | 79 | .. autoclass:: flask_user.forms.RegisterForm 80 | :no-undoc-members: 81 | :no-inherited-members: 82 | 83 | -------- 84 | 85 | .. _ResendEmailConfirmationForm: 86 | 87 | ResendEmailConfirmationForm 88 | --------------------------- 89 | 90 | .. autoclass:: flask_user.forms.ResendEmailConfirmationForm 91 | :no-undoc-members: 92 | :no-inherited-members: 93 | 94 | -------- 95 | 96 | .. _ResetPasswordForm: 97 | 98 | ResetPasswordForm 99 | ----------------- 100 | 101 | .. autoclass:: flask_user.forms.ResetPasswordForm 102 | :no-undoc-members: 103 | :no-inherited-members: 104 | -------------------------------------------------------------------------------- /docs/source/api_user_manager.rst: -------------------------------------------------------------------------------- 1 | .. _UserManagerClass: 2 | 3 | UserManager Class 4 | ================= 5 | 6 | The UserManager class implements most of the Flask-User functionality. 7 | 8 | Flask-User can be customized by extending or overriding any of the methods listed below. 9 | 10 | .. autoclass:: flask_user.user_manager.UserManager 11 | :no-undoc-members: 12 | :no-inherited-members: 13 | 14 | .. seealso:: 15 | 16 | - :ref:`UserManager__Settings` 17 | - :ref:`UserManager__Views` 18 | - :ref:`UserManager__Utils` 19 | 20 | -------------------------------------------------------------------------------- /docs/source/api_user_manager__settings.rst: -------------------------------------------------------------------------------- 1 | .. _UserManager__Settings: 2 | 3 | UserManager Settings 4 | ==================== 5 | 6 | Below is a complete list of configurable Flask-User settings. 7 | 8 | .. This hack shows a header above the _next_ section 9 | .. code-block:: none 10 | 11 | | The UserManager__Settings class mixes into the UserManager class. 12 | | Mixins allow for maintaining code and docs across several files. 13 | 14 | .. autoclass:: flask_user.user_manager__settings.UserManager__Settings 15 | :no-undoc-members: 16 | 17 | .. seealso:: 18 | 19 | - :ref:`UserManagerClass` 20 | - :ref:`UserManager__Views` 21 | - :ref:`UserManager__Utils` 22 | -------------------------------------------------------------------------------- /docs/source/api_user_manager__utils.rst: -------------------------------------------------------------------------------- 1 | .. _UserManager__Utils: 2 | 3 | UserManager Utility methods 4 | =========================== 5 | 6 | Below is a complete list of Flask-User utility methods. 7 | 8 | .. This hack shows a header above the _next_ section 9 | .. code-block:: none 10 | 11 | | The UserManager__Utils class mixes into the UserManager class. 12 | | Mixins allow for maintaining code and docs across several files. 13 | 14 | .. autoclass:: flask_user.user_manager__utils.UserManager__Utils 15 | :no-undoc-members: 16 | 17 | .. seealso:: 18 | 19 | - :ref:`UserManagerClass` 20 | - :ref:`UserManager__Settings` 21 | - :ref:`UserManager__Views` 22 | -------------------------------------------------------------------------------- /docs/source/api_user_manager__views.rst: -------------------------------------------------------------------------------- 1 | .. _UserManager__Views: 2 | 3 | UserManager View methods 4 | ======================== 5 | 6 | Below is a complete list of customizable Flask-User views. 7 | 8 | .. This hack shows a header above the _next_ section 9 | .. code-block:: none 10 | 11 | | The UserManager__Views class mixes into the UserManager class. 12 | | Mixins allow for maintaining code and docs across several files. 13 | 14 | .. autoclass:: flask_user.user_manager__views.UserManager__Views 15 | :no-undoc-members: 16 | 17 | .. seealso:: 18 | 19 | - :ref:`UserManagerClass` 20 | - :ref:`UserManager__Settings` 21 | - :ref:`UserManager__Utils` 22 | -------------------------------------------------------------------------------- /docs/source/authorization.rst: -------------------------------------------------------------------------------- 1 | Role-based Authorization 2 | ======================== 3 | Authorization is the process of specifying and enforcing access rights of users to resources. 4 | 5 | Flask-User offers role-based authorization through the use of the ``@roles_required`` decorator. 6 | 7 | @roles_required 8 | --------------- 9 | If a view function is decorated with the ``@roles_required`` decorator, the user: 10 | 11 | - must be logged in, and 12 | - must be associated with the specified role names. 13 | 14 | If any of these conditions is not met, an 'Unauthorized access' error message will be shown 15 | and the user will be redirected to the ``USER_UNAUTHORIZED_ENDPOINT``. 16 | 17 | In the example below the current user is required to be logged in and to be associated with the role named 'Admin':: 18 | 19 | from flask_user import roles_required 20 | 21 | @route('/admin/dashboard') # @route() must always be the outer-most decorator 22 | @roles_required('Admin') 23 | def admin_dashboard(): 24 | # render the admin dashboard 25 | 26 | Note: Comparison of role names is case sensitive, so ``'Admin'`` will NOT match ``'admin'``. 27 | 28 | Simple AND/OR operations 29 | ~~~~~~~~~~~~~~~~~~~~~~~~ 30 | 31 | The @roles_required decorator accepts one or more role names. 32 | At the decorator level, if multiple role names are specified here, 33 | the user must have **all** the specified roles. 34 | This is the AND operation. 35 | 36 | At the argument level, each item may be a role name or a list or role names. 37 | If a list of role names is specified here, 38 | the user mast have **any one** of the specified roles to gain access. 39 | This is the OR operation. 40 | 41 | In the example below, the user must always have the ``'Starving'`` role, 42 | AND either the ``'Artist'`` role OR the ``'Programmer'`` role:: 43 | 44 | # Ensures that the user is ('Starving' AND (an 'Artist' OR a 'Programmer')) 45 | @roles_required('Starving', ['Artist', 'Programmer']) 46 | 47 | Note: The nesting level only goes as deep as this example shows. 48 | 49 | 50 | Required Role and UserRoles data-models 51 | --------------------------------------- 52 | The @roles_required decorator depends the ``Role`` and ``UserRoles`` data-models 53 | (in addition to the ``User`` data-model). 54 | 55 | See the docs on :ref:`Role and UserRoles data-models`. 56 | 57 | Example App 58 | ----------- 59 | The :doc:`basic_app` demonstrates the use of the ``@roles_required`` decorator. 60 | -------------------------------------------------------------------------------- /docs/source/base_templates.rst: -------------------------------------------------------------------------------- 1 | Base templates 2 | ============== 3 | 4 | **templates/base.html** 5 | 6 | All Flask-User forms extend from the template file ``tempates/base.h`` and 7 | Flask-User supplies a built-in version that uses Bootstrap 3. 8 | 9 | To make Flask-User use your page template, you will need to create a ``base.html`` template 10 | file in your application's ``templates`` directory. 11 | 12 | Use ``{% block content %}{% endblock %}`` as a placeholder for the forms. 13 | 14 | **templates/flask_user/_public_base.html** 15 | 16 | Public forms are forms that do not require a logged-in user: 17 | 18 | * ``templates/flask_user/forgot_password.html``, 19 | * ``templates/flask_user/login.html``, 20 | * ``templates/flask_user/login_or_register.html``, 21 | * ``templates/flask_user/register.html``, 22 | * ``templates/flask_user/request_email_confirmation.html``, and 23 | * ``templates/flask_user/reset_password.html``. 24 | 25 | Public forms extend the template file ``templates/flask_user/_public_base.html``, 26 | which by default extends the template file ``templates/base.html``. 27 | 28 | If you want the public forms to use a base template file other than ``templates/base.html``, 29 | create the ``templates/flask_user/_public_base.html`` file in your application's 30 | ``templates`` directory with the following content:: 31 | 32 | {% extends 'my_public_base.html' %} 33 | 34 | **templates/flask_user/_authorized_base.html** 35 | 36 | Member forms are forms that require a logged-in user: 37 | 38 | * ``templates/flask_user/change_password.html``, 39 | * ``templates/flask_user/change_username.html``, and 40 | * ``templates/flask_user/manage_emails.html``. 41 | 42 | Member forms extend the template file ``templates/flask_user/_authorized_base.html``, 43 | which by default extends the template file ``templates/base.html``. 44 | 45 | If you want the member forms to use a base template file other than ``templates/base.html``, 46 | create the ``templates/flask_user/_authorized_base.html`` file in your application's 47 | ``templates`` directory with the following content:: 48 | 49 | {% extends 'my_authorized_base.html' %} 50 | 51 | **Summary** 52 | 53 | The following template files reside in the ``templates`` directory:: 54 | 55 | base.html # root template 56 | 57 | flask_user/_authorized_base.html # extends base.html 58 | flask_user/change_password.html # extends flask_user/_authorized_base.html 59 | flask_user/change_username.html # extends flask_user/_authorized_base.html 60 | flask_user/manage_emails.html # extends flask_user/_authorized_base.html 61 | 62 | flask_user/_public_base.html # extends base.html 63 | flask_user/forgot_password.html # extends flask_user/_public_base.html 64 | flask_user/login.html # extends flask_user/_public_base.html 65 | flask_user/login_or_register.html # extends flask_user/_public_base.html 66 | flask_user/register.html # extends flask_user/_public_base.html 67 | flask_user/request_email_confirmation.html # extends flask_user/_public_base.html 68 | flask_user/reset_password.html # extends flask_user/_public_base.html 69 | -------------------------------------------------------------------------------- /docs/source/basic_app.rst: -------------------------------------------------------------------------------- 1 | Basic App 2 | ========= 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/quickstart_submenu.rst 5 | 6 | -------- 7 | 8 | The Basic App builds on the QuickStart App by adding the following features: 9 | 10 | - Register and Login with an email 11 | - Confirm emails, Change passwords 12 | - Role-base authorization 13 | - Enable translations 14 | 15 | Unlike the QuickStart App, the Basic App requires proper SMTP settings 16 | and the installation of ``Flask-BabelEx``. 17 | 18 | Create a development environment 19 | -------------------------------- 20 | We recommend making use of virtualenv and virtualenvwrapper:: 21 | 22 | # Create virtual env 23 | mkvirtualenv my_app 24 | workon my_app 25 | 26 | # Create working directory 27 | mkdir -p ~dev/my_app # or mkdir C:\dev\my_app 28 | cd ~/dev/my_app # or cd C:\dev\my_app 29 | 30 | Install required Python packages 31 | -------------------------------- 32 | :: 33 | 34 | # Uninstall Flask-Babel if needed 35 | pip uninstall Flask-Babel 36 | 37 | # Install Flask-BabelEx and Flask-User 38 | pip install Flask-BabelEx 39 | pip install Flask-User 40 | 41 | Create the basic_app.py file 42 | ---------------------------- 43 | 44 | - Open your favorite editor, 45 | - Copy the example below, and 46 | - Save it as ~/dev/my_app/basic_app.py 47 | 48 | .. literalinclude:: ../../example_apps/basic_app.py 49 | :language: python 50 | :linenos: 51 | :emphasize-lines: 11, 25-32, 56-57, 88-89, 133, 151 52 | 53 | - Lines 25-32 configure ``Flask-Mail``. Make sure to use the correct settings or emails 54 | will not be sent. 55 | 56 | Configure Flask-Mail 57 | -------------------- 58 | Make sure to properly configure Flask-Mail settings:: 59 | 60 | # Flask-Mail SMTP Server settings 61 | MAIL_SERVER = 62 | MAIL_PORT = 63 | MAIL_USE_SSL = 64 | MAIL_USE_TLS = 65 | 66 | # Flask-Mail SMTP Account settings 67 | MAIL_USERNAME = 68 | MAIL_PASSWORD = 69 | 70 | .. note:: 71 | 72 | Gmail and Yahoo mail nowadays disable SMTP requests by default. 73 | Search the web for 'Gmail less secure apps' or 'Yahoo less secure apps' 74 | to learn how to change this default setting for your account. 75 | 76 | See :doc:`quickstart_app` for an example without SMTP. 77 | 78 | 79 | Configure your browser 80 | ---------------------- 81 | If you want to see translation in action, you will need to change and prioritize 82 | a :ref:`supported language ` (one that is other than 'English') 83 | in your browser language preferences. 84 | 85 | For Google Chrome: 86 | 87 | - Chrome > Preferences. Search for 'Language'. 88 | - Expand the 'Language' bar > Add languages. 89 | - Check the 'Dutch' checkbox > Add. 90 | - Make sure to move it to the top: Three dots > Move to top. 91 | 92 | 93 | Run the Basic App 94 | ----------------- 95 | Run the Basic App with the following command:: 96 | 97 | cd ~/dev/my_app 98 | python basic_app.py 99 | 100 | And point your browser to ``http://localhost:5000``. 101 | 102 | 103 | Troubleshooting 104 | --------------- 105 | 106 | If you receive an EmailError message, or if the Registration form does not respond quickly 107 | then you may have specified incorrect SMTP settings. 108 | 109 | If you receive a SQLAlchemy error message, you may be using an old DB schema. 110 | Delete the quickstart_app.sqlite file and restart the app. 111 | 112 | If you don't see any translations, you may not have installed ``Flask-BabelEx``, 113 | or you may not have prioritized a supported language in your browser settings. 114 | 115 | -------- 116 | 117 | .. include:: includes/submenu_defs.rst 118 | .. include:: includes/quickstart_submenu.rst 119 | -------------------------------------------------------------------------------- /docs/source/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGES.rst -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Flask-User documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Aug 25 06:58:40 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../../')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.napoleon', 37 | 'sphinx.ext.todo', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'Flask-User' 54 | copyright = '2017, Ling Thio' 55 | author = 'Ling Thio' 56 | 57 | # The version info for the project you're documenting, acts as replacement for 58 | # |version| and |release|, also used in various other places throughout the 59 | # built documents. 60 | # 61 | # The short X.Y version. 62 | version = '1.0' 63 | # The full version, including alpha/beta/rc tags. 64 | release = 'v1.0' 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = None 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This patterns also effect to html_static_path and html_extra_path 76 | exclude_patterns = [] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = 'sphinx' 80 | 81 | # If true, `todo` and `todoList` produce output, else they produce nothing. 82 | todo_include_todos = True 83 | 84 | 85 | # -- Options for HTML output ---------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | html_theme = 'alabaster' 90 | 91 | # html_theme_options = { 92 | # } 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['_static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # This is required for the alabaster theme 109 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 110 | html_sidebars = { 111 | '**': [ 112 | 'about.html', 113 | 'globaltoc.html', 114 | 'searchbox.html', 115 | ] 116 | } 117 | # html_sidebars = { 118 | # '**': [ 119 | # 'about.html', 120 | # 'navigation.html', 121 | # 'relations.html', # needs 'show_related': True theme option to display 122 | # 'searchbox.html', 123 | # 'donate.html', 124 | # ] 125 | # } 126 | 127 | 128 | # -- Options for HTMLHelp output ------------------------------------------ 129 | 130 | # Output file base name for HTML help builder. 131 | htmlhelp_basename = 'Flask-Userdoc' 132 | 133 | 134 | # -- Options for LaTeX output --------------------------------------------- 135 | 136 | latex_elements = { 137 | # The paper size ('letterpaper' or 'a4paper'). 138 | # 139 | # 'papersize': 'letterpaper', 140 | 141 | # The font size ('10pt', '11pt' or '12pt'). 142 | # 143 | # 'pointsize': '10pt', 144 | 145 | # Additional stuff for the LaTeX preamble. 146 | # 147 | # 'preamble': '', 148 | 149 | # Latex figure (float) alignment 150 | # 151 | # 'figure_align': 'htbp', 152 | } 153 | 154 | # Grouping the document tree into LaTeX files. List of tuples 155 | # (source start file, target name, title, 156 | # author, documentclass [howto, manual, or own class]). 157 | latex_documents = [ 158 | (master_doc, 'Flask-User.tex', 'Flask-User Documentation', 159 | 'Ling Thio', 'manual'), 160 | ] 161 | 162 | 163 | # -- Options for manual page output --------------------------------------- 164 | 165 | # One entry per manual page. List of tuples 166 | # (source start file, name, description, authors, manual section). 167 | man_pages = [ 168 | (master_doc, 'flask-user', 'Flask-User Documentation', 169 | [author], 1) 170 | ] 171 | 172 | 173 | # -- Options for Texinfo output ------------------------------------------- 174 | 175 | # Grouping the document tree into Texinfo files. List of tuples 176 | # (source start file, target name, title, author, 177 | # dir menu entry, description, category) 178 | texinfo_documents = [ 179 | (master_doc, 'Flask-User', 'Flask-User Documentation', 180 | author, 'Flask-User', 'One line description of project.', 181 | 'Miscellaneous'), 182 | ] 183 | 184 | # -- Autodoc --- 185 | add_module_names = False # Remove module paths from docs 186 | autodoc_default_flags = ['members', 'undoc-members', 'inherited-members'] 187 | autodoc_member_order = 'bysource' 188 | autoclass_content = 'both' # Show class doc, but not __init__ doc 189 | 190 | def setup(app): 191 | # Add custom CSS with: 192 | # - Disable word hyphenation by HTML/CSS 193 | # - Smaller font size for code blocks 194 | app.add_stylesheet('custom.css') 195 | 196 | -------------------------------------------------------------------------------- /docs/source/configuring_settings.rst: -------------------------------------------------------------------------------- 1 | .. _ConfiguringSettings: 2 | 3 | Configuring settings 4 | ==================== 5 | .. include:: includes/submenu_defs.rst 6 | .. include:: includes/customizing_submenu.rst 7 | 8 | -------- 9 | 10 | Flask-User default features and settings can overridden through the app config:: 11 | 12 | # Customize Flask-User settings 13 | USER_ENABLE_EMAIL = True 14 | USER_ENABLE_USERNAME = False 15 | 16 | Flask-User settings 17 | ------------------- 18 | 19 | Below is a complete list of configurable Flask-User settings and their defaults. 20 | 21 | Note: Ignore the `__Settings` part of the class name. 22 | It's a trick we use to split the code and docs across several files. 23 | 24 | .. autoclass:: flask_user.user_manager__settings.UserManager__Settings 25 | :private-members: 26 | :noindex: 27 | 28 | 29 | To keep the code base simple and robust, we offer no easy way to change 30 | the '/user' base URLs or the '/flask_user' base directories in bulk. 31 | Please copy them from this page, then use your editor to bulk-change these settings. 32 | 33 | -------- 34 | 35 | .. include:: includes/submenu_defs.rst 36 | .. include:: includes/customizing_submenu.rst 37 | -------------------------------------------------------------------------------- /docs/source/customizing.rst: -------------------------------------------------------------------------------- 1 | Customizing Flask-User 2 | ====================== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/customizing_submenu.rst 5 | 6 | -------- 7 | 8 | From the very beginning, Flask-User was specifically designed to be fully customizable 9 | in addition to being secure and reliable. 10 | 11 | - Flask-User can be configured through :doc:`configuring_settings`. 12 | - Enabling and disabling of features 13 | - General settings 14 | - Token expiration settings 15 | - Complete control of Passlib's password hash settings 16 | - Form URLs 17 | - Form template file paths 18 | - Email template file paths 19 | 20 | - :doc:`customizing_forms` 21 | - :ref:`CustomizingFormTemplates` 22 | - :ref:`CustomizingFormClasses` 23 | - :ref:`CustomizingFormValidators` 24 | - :ref:`CustomizingFormViews` 25 | 26 | - :doc:`customizing_emails` 27 | - :ref:`CustomizingEmailTemplates` 28 | 29 | - And then there are :doc:`customizing_advanced` 30 | - :ref:`CustomizingManagers` 31 | - :ref:`CustomDbAdapters` 32 | - :ref:`CustomEmailAdapters` 33 | 34 | 35 | -------- 36 | 37 | .. include:: includes/submenu_defs.rst 38 | .. include:: includes/customizing_submenu.rst 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/source/customizing_advanced.rst: -------------------------------------------------------------------------------- 1 | Advanced Customizations 2 | ======================= 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/customizing_submenu.rst 5 | 6 | -------- 7 | 8 | .. _CustomizingManagers: 9 | 10 | Customizing the Email, Password and Token Managers 11 | -------------------------------------------------- 12 | 13 | Developers can customize the EmailManager, the PasswordManager, and the TokenManager as follows:: 14 | 15 | # Customize the EmailManager 16 | from flask_user import EmailManager 17 | class CustomEmailManager(EmailManager): 18 | ... 19 | 20 | # Customize the PasswordManager 21 | from flask_user import PasswordManager 22 | class CustomPasswordManager(PasswordManager): 23 | ... 24 | 25 | # Customize the TokenManager 26 | from flask_user import TokenManager 27 | class CustomTokenManager(TokenManager): 28 | ... 29 | 30 | # Setup Flask-User 31 | user_manager = UserManager(app, db, User) 32 | 33 | # Customize Flask-User managers 34 | user_manager.email_manager = CustomEmailManager(app) 35 | user_manager.password_manager = CustomPasswordManager(app, 'bcrypt') 36 | user_manager.token_manager = CustomTokenManager(app) 37 | 38 | .. seealso:: 39 | 40 | | :ref:`EmailManager`, 41 | | :ref:`PasswordManager`, and 42 | | :ref:`TokenManager`. 43 | 44 | -------- 45 | 46 | Implementing a CustomDbAdapter 47 | ------------------------------ 48 | 49 | -------- 50 | 51 | See :ref:`CustomDbAdapters` 52 | 53 | Implementing a CustomEmailAdapter 54 | --------------------------------- 55 | 56 | See :ref:`CustomEmailAdapters` 57 | 58 | -------- 59 | 60 | .. include:: includes/submenu_defs.rst 61 | .. include:: includes/customizing_submenu.rst 62 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | F.A.Q. 3 | ====== 4 | 5 | | **Q: Can I see a preview?** 6 | | A: Yes you can: `Flask-User Demo `_ 7 | 8 | | **Q: What's the relationship between Flask-User and Flask-Login?** 9 | | A: Flask-User manages **users** and uses Flask-Login to manage **user sessions**. 10 | | Flask-User is built on top of Flask-Login to provide the register/login/change forms and to manages the email verification, the user authentication and the user authorization. 11 | 12 | | **Q: What are the differences between Flask-User and Flask-Security?** 13 | | A: The main reason why I wrote Flask-User was because I found it difficult to customize 14 | Flask-Security messages and functionality (in 2013) and because it didn't offer 15 | Username login, multiple emails per user, and Internationalization. 16 | 17 | Flask-User has been designed with :doc:`Full customization ` in mind 18 | and additionally offers Username login and Internationalization. 19 | It exists since December 2013 and contains 661 statements with a 98% test coverage. 20 | 21 | Flask-Security has been around since at least March 2012 22 | and additionally offers Json/AJAX, MongoDB, Peewee, and Basic HTTP Authentication. 23 | 24 | | **Q: Can users login with usernames and email addresses?** 25 | | A: Yes. 26 | Flask-User can be configured to enable usernames, email addresses or both. 27 | If both are enabled, 28 | users can log in with either their username or their email address. 29 | 30 | | **Q: Does Flask-User work with existing hashed passwords?** 31 | | A: Yes. It supports the following: 32 | | - passwords hashed by any ``passlib`` hashing algorithm (via a config setting) 33 | | - passwords hashed by Flask-Security (via a config setting) 34 | | - custom password hashes (via custom functions) 35 | 36 | | **Q: What databases does Flask-User support?** 37 | | A: Any database that SQLAlchemy supports (via SqlAlchemyAdapter) 38 | | and other databases (via custom DBAdapters) 39 | 40 | Flask-User shields itself from database operations through a DBAdapter. 41 | It ships with a SQLAlchemyAdapter, but the API is very simple, so other adapters 42 | can be easily added. See :ref:`DbAdapterInterface`. 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/source/includes/customizing_submenu.rst: -------------------------------------------------------------------------------- 1 | |bullet| :doc:`Intro ` |spacing| 2 | |bullet| :doc:`Settings ` |spacing| 3 | |bullet| :doc:`Forms ` |spacing| 4 | |bullet| :doc:`Emails ` |spacing| 5 | |bullet| :doc:`Advanced ` |spacing| 6 | -------------------------------------------------------------------------------- /docs/source/includes/main_page.rst: -------------------------------------------------------------------------------- 1 | Flask-User v1.0 2 | =============== 3 | **Customizable User Authentication, User Management, and more.** 4 | 5 | | So, you're writing a Flask web application and would like to authenticate your users. 6 | | You start with a simple **Login** page, but soon enough you'll need to handle: 7 | 8 | * **Registrations** and **Email Confirmations** 9 | * **Change Usernames**, **Change Passwords**, and **Forgotten Passwords** 10 | 11 | .. _SupportedLanguages: 12 | 13 | And wouldn't it be nice to also offer: 14 | 15 | * **Added Security** 16 | * **Increased Reliability** 17 | * **Role-based Authorization** 18 | * **Internationalization** (Chinese, Dutch, English, Farsi, Finnish, French, German, Italian, Polish, Russian, Slovak, Spanish, Swedish, Turkish and Ukrainian) 19 | 20 | Stable 21 | ------ 22 | Version 1.0 is Production/Stable grade since 2018. 23 | 24 | Customizable, yet Ready to use 25 | ------------------------------ 26 | * **Largely Configurable** -- By overriding configuration settings. 27 | * **Fully Customizable** -- By overriding methods and properties. 28 | * **Ready to use** -- Through sensible defaults. 29 | * Supports **SQL** and **MongoDB** databases. 30 | 31 | Well documented 32 | --------------- 33 | - `Flask-User v1.0 documentation `_ 34 | - `Flask-User v0.6 documentation `_ 35 | - `Flask-User v0.5 documentation `_ 36 | 37 | Additional features 38 | ------------------- 39 | * **MIT License** 40 | * **Tested** on Python 2.6, 2.7, 3.3, 3.4, 3.5 and 3.6. Coverage: Over 90%. 41 | * **Event hooking** -- Through efficient signals. 42 | * **Support for multiple emails per user** 43 | 44 | Minimal Requirements 45 | -------------------- 46 | - bcrypt 2.0+ 47 | - cryptography 1.6+ 48 | - Flask 0.9+ 49 | - Flask-Login 0.2+ 50 | - Flask-WTF 0.9+ 51 | - passlib 1.7+ 52 | 53 | Alternatives 54 | ------------ 55 | * `Flask-Login `_ 56 | * `Flask-Security `_ 57 | -------------------------------------------------------------------------------- /docs/source/includes/porting_submenu.rst: -------------------------------------------------------------------------------- 1 | |bullet| :doc:`Porting ` |spacing| 2 | |bullet| :doc:`Basics ` |spacing| 3 | |bullet| :doc:`Customizations ` |spacing| 4 | |bullet| :doc:`Advanced ` |spacing| 5 | -------------------------------------------------------------------------------- /docs/source/includes/quickstart_submenu.rst: -------------------------------------------------------------------------------- 1 | |bullet| :doc:`Intro ` |spacing| 2 | |bullet| :doc:`quickstart_app` |spacing| 3 | |bullet| :doc:`basic_app` |spacing| 4 | |bullet| :doc:`mongodb_app` |spacing| 5 | -------------------------------------------------------------------------------- /docs/source/includes/signals.txt: -------------------------------------------------------------------------------- 1 | :: 2 | 3 | # Signal # Sent when ... 4 | user_changed_password # a user changed their password 5 | user_changed_username # a user changed their username 6 | user_confirmed_email # a user confirmed their email 7 | user_forgot_password # a user submitted a reset password request 8 | user_logged_in # a user logged in 9 | user_logged_out # a user logged out 10 | user_registered # a user registered for a new account 11 | user_reset_password # a user reset their password (forgot password) 12 | -------------------------------------------------------------------------------- /docs/source/includes/submenu_defs.rst: -------------------------------------------------------------------------------- 1 | .. |bullet| raw:: html 2 | 3 | • 4 | 5 | .. |spacing| raw:: html 6 | 7 |       8 | -------------------------------------------------------------------------------- /docs/source/includes/template_functions.txt: -------------------------------------------------------------------------------- 1 | :: 2 | 3 | # Function Setting # Default 4 | url_for('user.change_password') USER_CHANGE_PASSWORD_URL = '/user/change-password' 5 | url_for('user.change_username') USER_CHANGE_USERNAME_URL = '/user/change-username' 6 | url_for('user.confirm_email') USER_CONFIRM_EMAIL_URL = '/user/confirm-email/' 7 | url_for('user.email_action') USER_EMAIL_ACTION_URL = '/user/email//' # v0.5.1 and up 8 | url_for('user.forgot_password') USER_FORGOT_PASSWORD_URL = '/user/forgot-password' 9 | url_for('user.login') USER_LOGIN_URL = '/user/sign-in' 10 | url_for('user.logout') USER_LOGOUT_URL = '/user/sign-out' 11 | url_for('user.register') USER_REGISTER_URL = '/user/register' 12 | url_for('user.resend_email_confirmation') USER_RESEND_EMAIL_CONFIRMATION_URL = '/user/resend-email-confirmation' # v0.5.0 and up 13 | url_for('user.reset_password') USER_RESET_PASSWORD_URL = '/user/reset-password/' 14 | url_for('user.profile') USER_PROFILE_URL = '/user/profile' # v0.5.5 and up 15 | -------------------------------------------------------------------------------- /docs/source/includes/template_variables.txt: -------------------------------------------------------------------------------- 1 | :: 2 | 3 | user_manager # points to the UserManager object 4 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: includes/main_page.rst 2 | .. include:: ../../AUTHORS.rst 3 | 4 | Table of Contents 5 | ----------------- 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | Flask-User 10 | installation 11 | quickstart 12 | limitations 13 | data_models 14 | authorization 15 | Customizing 16 | internationalization 17 | porting 18 | changes 19 | api 20 | 21 | .. toctree:: 22 | :hidden: 23 | 24 | _todo 25 | quickstart_app 26 | basic_app 27 | mongodb_app 28 | api_db_adapters 29 | api_decorators 30 | api_email_adapters 31 | api_forms 32 | api_user_manager 33 | api_user_manager__settings 34 | api_user_manager__utils 35 | api_user_manager__views 36 | configuring_settings 37 | customizing_emails 38 | customizing_forms 39 | customizing_advanced 40 | porting_basics 41 | porting_customizations 42 | porting_advanced 43 | base_templates 44 | signals 45 | recipes 46 | faq 47 | old_api 48 | unused 49 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | We recommend making use of virtualenv, virtualenvwrapper and pip:: 6 | 7 | # Create virtual enviroment 8 | mkvirtualenv my_app 9 | 10 | # Switch to virtual environment 11 | workon my_app 12 | 13 | # Install Flask-User 14 | pip install Flask-User 15 | 16 | Optional uninstalls 17 | ------------------- 18 | - FLask-User installs bcrypt for its default passlib hash of 'bcrypt'. 19 | - Flask-User installs Flask-Mail for its default SMTPEmailAdapter. 20 | - Flask-User installs Flask-SQLAlchemy for its default SQLDbAdapter. 21 | 22 | If you configure/customize Flask-User away from their defaults, certain packages may be uninstalled:: 23 | 24 | # Optionally uninstall unused packages 25 | pip uninstall bcrypt 26 | pip uninstall Flask-Mail 27 | pip uninstall Flask-SQLAlchemy 28 | 29 | -------------------------------------------------------------------------------- /docs/source/internationalization.rst: -------------------------------------------------------------------------------- 1 | Internationalization 2 | ==================== 3 | Flask-User ships with the following language translations: 4 | 5 | * Chinese Simplified (zh) 6 | * Dutch (nl) 7 | * English (en) 8 | * German (de) 9 | * Farsi (fa) 10 | * Finnish (fi) 11 | * French (fr) 12 | * Italian (it) 13 | * Polish (pl) 14 | * Russian (ru) 15 | * Slovak (sk) 16 | * Spanish (es) 17 | * Swedish (sv) 18 | * Turkish (tr) 19 | * Ukrainian (uk) 20 | 21 | They can be found in the Flask-User installation directory, under the ``translations`` subdirectory. 22 | Each translation file is called ``flask_user.mo`` (called a domain translation) 23 | to differentiate from your application's translations, typically called ``messages.mo`` 24 | 25 | REQUIRED: Installing Flask-BabelEx 26 | ---------------------------------- 27 | There are two distinct Flask extensions for managing translations: ``Flask-Babel`` 28 | and ``Flask-BabelEx``. 29 | 30 | Flask-User relies on the domain-specific abilities of ``Flask-BabelEx`` 31 | and will not translate with ``Flask-Babel``:: 32 | 33 | # Uninstall Flask-Babel if needed 34 | pip uninstall Flask-Babel 35 | 36 | # Install Flask-BabelEx 37 | pip install Flask-BabelEx 38 | 39 | REQUIRED: Initializing Flask-BabelEx 40 | ------------------------------------ 41 | 42 | Flask-BabelEx must be initialized: 43 | 44 | - After Flask-User initialization 45 | - After the app config has been loaded 46 | - Before Flask-User initialization 47 | 48 | Example:: 49 | 50 | from flask import Flask, request 51 | from flask_babelex import Babel 52 | from flask_user import UserManager 53 | ... 54 | # Setup Flask 55 | app = Flask(__name__) 56 | ... 57 | # Read applicaton config 58 | app.config.from_object('app.config.settings') 59 | ... 60 | # Initialize Flask-BabelEx 61 | babel = Babel(app) 62 | ... 63 | # Initialize Flask-User 64 | user_manager = UserManager(app, db, User) 65 | 66 | REQUIRED: Setting your browser language preference 67 | -------------------------------------------------- 68 | You will need to add and prioritize one of the Flask-User supported languages 69 | in your browser. 70 | 71 | For Google Chrome: 72 | 73 | - Chrome > Preferences. Search for 'Language'. 74 | - Expand the 'Language' bar > Add languages. 75 | - Check the 'Dutch' checkbox > Add. 76 | - Make sure to move it to the top: Three dots > Move to top. 77 | 78 | You can test this with the :doc:`basic_app`. 79 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | .. include:: includes/main_page.rst 2 | .. include:: ../../AUTHORS.rst 3 | 4 | -------------------------------------------------------------------------------- /docs/source/limitations.rst: -------------------------------------------------------------------------------- 1 | .. _limitations: 2 | 3 | =========== 4 | Limitations 5 | =========== 6 | 7 | We want to be transparent about what this package can and can not do. 8 | 9 | 10 | Python versions 11 | --------------- 12 | Flask-User has been tested with Python 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. 13 | 14 | 15 | Flask versions 16 | -------------- 17 | Flask-User works with Flask 0.9+ 18 | 19 | 20 | Supported Databases 21 | ------------------- 22 | Flask-User makes use of DbAdapters to support different databases. 23 | 24 | It ships with a SQLDbAdapter to support a wide range of SQL databases via Flask-SQLAlchemy 25 | (Firebird, Microsoft SQL Server, MySQL, Oracle, PostgreSQL, SQLite, Sybase and more). 26 | 27 | It ships with a MongoDbAdapter to support MongoDB databases via Flask-MongoEngine. 28 | 29 | Custom DbAdapters can be implemented to support other Databases. 30 | 31 | 32 | Supported Email Mailers 33 | ----------------------- 34 | Flask-User makes use of EmailAdapters to send email via several platforms. 35 | 36 | It ships with a SMTPEmailAdapter a SendmailEmailAdapter and a SendGridEmailAdapter 37 | to send emails via SMTP, ``sendmail`` and SendGrid. 38 | 39 | Custom EmailAdapters can be implemented to support other Email Mailers. 40 | 41 | 42 | Fixed app.user_manager name 43 | --------------------------- 44 | 45 | An initialized UserManager() instance will assign itself to the ``app.user_manager`` property. 46 | This ``app.user_manager`` name can not be changed. 47 | 48 | 49 | Fixed data-model property names 50 | -------------------------------- 51 | 52 | The following data-model property names are fixed:: 53 | 54 | User.id 55 | User.password 56 | User.username # optional 57 | User.email # optional 58 | User.email_confirmed_at # optional 59 | User.active # optional 60 | User.roles # optional 61 | User.user_emails # optional 62 | Role.name # optional 63 | UserEmail.id # optional 64 | UserEmail.email # optional 65 | UserEmail.email_confirmed_at # optional 66 | UserInvitation.id # optional 67 | UserInvitation.email # optional 68 | UserInvitation.invited_by_user_id # optional 69 | 70 | 71 | If you have existing code, and are unable to globally change a fixed property name, 72 | consider using Python's getter and setter properties as a bridge:: 73 | 74 | class User(db.Model, UserMixin): 75 | ... 76 | # Existing code uses email_address instead of email 77 | email_address = db.Column(db.String(255), nullable=False, unique=True) 78 | ... 79 | 80 | # define email getter 81 | @property 82 | def email(self): 83 | return self.email_address # on user.email: return user.email_address 84 | 85 | # define email setter 86 | @email.setter 87 | def email(self, value): 88 | self.email_address = value # on user.email='xyz': set user.email_address='xyz' 89 | 90 | 91 | Flexible data-model class, SQL table, and SQL column names 92 | ---------------------------------------------------------------- 93 | | Data-model class names are unrestricted. 94 | | SQL table names are unrestricted. 95 | | SQL column names are unrestricted. 96 | 97 | Here is an example of a data-model class with different class, table and column names:: 98 | 99 | # Use of the Member class name (instead of User) 100 | class Member(db.Model, UserMixin): 101 | 102 | # Use of the 'members' SQL table (instead of 'users') 103 | __tablename__ = 'members' 104 | ... 105 | # Use of the 'email_address' SQL column (instead of 'email') 106 | email = db.Column('email_address', db.String(255), nullable=False, unique=True) 107 | 108 | # Setup Flask-User 109 | user_manager = UserManager(app, db, Member) # Specify the Member class 110 | 111 | 112 | Primary keys 113 | ------------ 114 | Even though Flask-User relies on the following: 115 | 116 | - Primary key is a single property named ``id``. 117 | - ``id`` properties are: 118 | 119 | - integers, 120 | - or strings, 121 | - or offer a string representation with ``str(id)``. 122 | 123 | Developers can still support primary key properties named other than ``id``:: 124 | 125 | class User(db.Model, UserMixin): 126 | # Composite primary key 127 | pk = db.Column(db.Integer, primary_key=True) 128 | ... 129 | # Map: id=user.id to: id=user.pk 130 | @property 131 | def id(self): 132 | return self.pk 133 | 134 | # Map: user.id=id to: user.pk=id 135 | @id.setter 136 | def id(self, value): 137 | self.pk = value 138 | 139 | Developers can still support composite primary keys:: 140 | 141 | class User(db.Model, UserMixin): 142 | # Composite primary key 143 | pk1 = db.Column(db.Integer, primary_key=True) 144 | pk2 = db.Column(db.String, primary_key=True) 145 | ... 146 | # Map: id=user.id to: id=str(pk1)+'|'+pk2 147 | @property 148 | def id(self): 149 | return str(self.pk1)+'|'+self.pk2 # Naive concatenation 150 | 151 | # Map: user.id=str(pk1)+'|'+pk2 to: user.pk1=pk1; user.pk2=pk2; 152 | @id.setter 153 | def id(self, value): 154 | items = value.split('|',1) # Naive split 155 | self.pk1 = int(items[0]) 156 | self.pk2 = items[1] 157 | 158 | Developers can customize the TokenManager to accept IDs without string representations. 159 | -------------------------------------------------------------------------------- /docs/source/mongodb_app.rst: -------------------------------------------------------------------------------- 1 | MongoDB App 2 | =========== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/quickstart_submenu.rst 5 | 6 | -------- 7 | 8 | Flask-User can work with MongoDB databases by replacing the default SQLDbAdapter 9 | with the provided MongoDbAdapter. 10 | 11 | The MONGODB_SETTINGS in this example requires a MongoDB server running on localhost:27017 (its default). 12 | 13 | Install Flask-User 14 | ------------------ 15 | 16 | We recommend making use of virtualenv and virtualenvwrapper:: 17 | 18 | # Create virtual env 19 | mkvirtualenv my_app 20 | workon my_app 21 | 22 | # Create working directory 23 | mkdir -p ~dev/my_app # or mkdir C:\dev\my_app 24 | cd ~/dev/my_app # or cd C:\dev\my_app 25 | 26 | # Install Flask-User 27 | pip install flask-user 28 | 29 | Install Flask-MongoEngine 30 | ------------------------- 31 | 32 | :: 33 | 34 | # Install Flask-MongoEngine 35 | pip install Flask-MongoEngine 36 | 37 | 38 | Create the mongodb_app.py file 39 | ------------------------------ 40 | 41 | - Open your favorite editor, 42 | - Copy the example below, and 43 | - Save it as ``~/dev/my_app/mongodb_app.py`` 44 | 45 | .. literalinclude:: ../../example_apps/mongodb_app.py 46 | :language: python 47 | :linenos: 48 | :emphasize-lines: 9, 25-29, 43-44, 58-59, 79 49 | 50 | Highlighted lines shows the few additional Flask-User code lines. 51 | 52 | 53 | Run the MongoDB App 54 | ------------------- 55 | Run the MongoDB App with the following command:: 56 | 57 | cd ~/dev/my_app 58 | python mongodb_app.py 59 | 60 | And point your browser to ``http://localhost:5000``. 61 | 62 | 63 | Troubleshooting 64 | --------------- 65 | 66 | [TBD] 67 | 68 | -------- 69 | 70 | .. include:: includes/submenu_defs.rst 71 | .. include:: includes/quickstart_submenu.rst 72 | -------------------------------------------------------------------------------- /docs/source/old_api.rst: -------------------------------------------------------------------------------- 1 | Flask-User API 2 | ============== 3 | * `Template variables`_ 4 | * `Template functions`_ 5 | * `Signals`_ 6 | 7 | Template variables 8 | ------------------ 9 | The following template variables are available for use in email and form templates: 10 | 11 | .. include:: includes/template_variables.txt 12 | 13 | Template functions 14 | ------------------ 15 | The following template functions are available for use in email and form templates: 16 | 17 | .. include:: includes/template_functions.txt 18 | 19 | hash_password() 20 | ~~~~~~~~~~~~~~~ 21 | :: 22 | 23 | user_manager.hash_password(password) 24 | # Returns hashed 'password' using the configured password hash 25 | # Config settings: USER_PASSWORD_HASH_MODE = 'passlib' 26 | # USER_PASSWORD_HASH = 'bcrypt' 27 | # USER_PASSWORD_SALT = SECRET_KEY 28 | 29 | 30 | verify_password() 31 | ~~~~~~~~~~~~~~~~~~~~~~ 32 | :: 33 | 34 | user_manager.verify_password(password, user.password) 35 | # Returns True if 'password' matches the user's 'hashed password' 36 | # Returns False otherwise. 37 | 38 | Signals 39 | ------- 40 | .. include:: includes/signals.txt 41 | -------------------------------------------------------------------------------- /docs/source/porting.rst: -------------------------------------------------------------------------------- 1 | Porting v0.6 to v0.9+ 2 | ===================== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/porting_submenu.rst 5 | 6 | -------- 7 | 8 | Ever since Flask-User v0.4, we had plans to improve Flask-User but were held back 9 | by our desire to maintain backwards compatibility for a while. 10 | 11 | With Flask-User v1.0 (and its v1.0 alpha/beta) we decided it was time to move forward, 12 | breaking compatibility with v0.6. 13 | 14 | Porting slightly customized v0.6 applications 15 | --------------------------------------------- 16 | If you've only customized Flask-User v0.6 in the following ways: 17 | 18 | - Changed ``USER_...`` app config settings 19 | - Customized form templates (the .html files) 20 | - Customized email templates (the .html or .txt files) 21 | 22 | Reading :doc:`porting_basics` will suffice. 23 | 24 | Porting highly customized v0.6 applications 25 | ------------------------------------------- 26 | If you have: 27 | 28 | - Specified custom form classes (the .py files) 29 | - Specified custom view functions 30 | - Specified custom password or username validators 31 | - Specified a custom TokenManager 32 | - Used the optional UserAuth class 33 | 34 | you will also need to read: :doc:`porting_customizations`. 35 | 36 | Porting Tips 37 | ------------ 38 | See :doc:`porting_advanced` 39 | 40 | -------- 41 | 42 | .. include:: includes/porting_submenu.rst 43 | -------------------------------------------------------------------------------- /docs/source/porting_basics.rst: -------------------------------------------------------------------------------- 1 | Porting Basics 2 | ============== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/porting_submenu.rst 5 | 6 | -------- 7 | 8 | Package installs 9 | ---------------- 10 | :: 11 | 12 | # Remove unused Python packages 13 | pip uninstall Flask-User # Uninstall v0.6 14 | pip uninstall py-crypt # This may already be absent 15 | pip uninstall Flask-Babel # We're requiring Flask-BabelEx now 16 | 17 | # Install new Python packages 18 | pip install Flask-BabelEx # Only if you require internationalization 19 | pip install Flask-User # Install v1.0 20 | 21 | Use: ``pip freeze | grep Flask-User`` to show the installed Flask-User version, 22 | and update your requirements.txt file accordingly:: 23 | 24 | # In requirements.txt: 25 | Flask-User==0.9.{X} 26 | 27 | Flask-User setup 28 | ---------------- 29 | We've removed the need to specify the type of DbAdapter during application setup. 30 | 31 | From v0.6:: 32 | 33 | from flask_user import UserManager, UserMixin, SQLAlchemyAdapter 34 | ... 35 | # Setup Flask-User 36 | db_adapter = SQLAlchemyAdapter(db, User) 37 | user_manager = UserManager(db_adapter, app) 38 | 39 | To v1.0+:: 40 | 41 | from flask_user import UserManager, UserMixin # No SQLAlchemyAdapter here!! 42 | ... 43 | # Setup Flask-User 44 | user_manager = UserManager(app, db, User) 45 | 46 | Make sure to stop using the legacy SQLAlchemyAdapter or DbAdapter 47 | classes as they will trigger legacy warning exceptions. 48 | 49 | USER\_... config settings 50 | ------------------------- 51 | Some v0.6 ``USER_...`` settings have been renamed in v1.0 to better reflect 52 | what these settings means. v1.0 still honors the old v0.6 names, but 53 | a deprecation warning message will be printed. 54 | 55 | We recommend resolving these warning messages by renaming the following settings: 56 | 57 | .. code-block:: none 58 | 59 | Replace: USER_ENABLE_RETYPE_PASSWORD 60 | with: USER_REQUIRE_RETYPE_PASSWORD 61 | 62 | Replace: USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL 63 | with: USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL 64 | 65 | Replace: USER_SHOW_USERNAME_EMAIL_DOES_NOT_EXIST 66 | with: USER_SHOW_EMAIL_DOES_NOT_EXIST 67 | & USER_SHOW_USERNAME_DOES_NOT_EXIST 68 | 69 | Replace: MAIL_DEFAULT_SENDER = '"App name" ' 70 | with: USER_EMAIL_SENDER_NAME = 'App name' 71 | & USER_EMAIL_SENDER_EMAIL = info@example.com 72 | 73 | 74 | User.email_confirmed_at property 75 | -------------------------------- 76 | We renamed the ``User.confirmed_at`` property to ``User.email_confirmed_at`` 77 | to better reflect what it represents. 78 | 79 | Replace v0.6:: 80 | 81 | class User(db.Model, UserMixin) 82 | ... 83 | confirmed_at = db.Column(db.DateTime()) 84 | 85 | With v1.0+:: 86 | 87 | class User(db.Model, UserMixin) 88 | email_confirmed_at = db.Column('confirmed_at', db.DateTime()) 89 | 90 | Notice how SQLAlchemy allows us to keep using the old ``confirmed_at`` column name 91 | with the new ``email_confirmed_at`` property. 92 | 93 | See :doc:`porting_advanced` for a workaround if you can not rename this property. 94 | 95 | Form template customization 96 | --------------------------- 97 | No known porting steps are needed for customized .html files. 98 | 99 | Email template customization 100 | ---------------------------- 101 | No known porting steps are needed for customized .html and .txt files. 102 | 103 | Contact us 104 | ---------- 105 | We believe this concludes the Basic Porting steps for **for applications with 106 | minimal Flask-User customization**. 107 | 108 | If, after reading :doc:`porting_customizations` and :doc:`porting_advanced`, 109 | you still think this page is incomplete, please email ling.thio@gmail.com. 110 | 111 | -------- 112 | 113 | .. include:: includes/porting_submenu.rst 114 | -------------------------------------------------------------------------------- /docs/source/porting_customizations.rst: -------------------------------------------------------------------------------- 1 | Porting Customizations 2 | ====================== 3 | 4 | .. include:: includes/submenu_defs.rst 5 | .. include:: includes/porting_submenu.rst 6 | 7 | -------- 8 | 9 | Read this if you have: 10 | 11 | - Specified custom Form classes (the .py files) 12 | - Specified custom View functions 13 | - Specified custom Password or username validators 14 | - Specified a custom TokenManager 15 | - Used the optional UserAuth class 16 | 17 | This topic assumes that you completed the porting tasks described in :doc:`porting_basics`. 18 | 19 | UserManager customization 20 | ------------------------- 21 | In v0.6, Flask-User was customized by adding parameters to the UserManager() 22 | instantiation. For example:: 23 | 24 | # Setup Flask-User 25 | db_adapter = SQLAlchemyAdaper(db, User) 26 | user_manager = UserManager(db_adapter, app, 27 | register_form = CustomRegisterForm, 28 | register_view_function = custom_register_view, 29 | password_validator = custom_password_validator, 30 | token_manager = CustomTokenManager()) 31 | 32 | In v1.0, Flask-User is customized by: 33 | - Extending the ``CustomUserManager`` class 34 | - Setting properties in its ``customize()`` method 35 | - Overriding or extending methods 36 | 37 | :: 38 | 39 | # Customize Flask-User 40 | class CustomUserManager(UserManager): 41 | 42 | def customize(self, app): 43 | # Override properties 44 | self.RegisterFormClass = CustomRegisterForm 45 | self.token_manager = CustomTokenManager(app) 46 | 47 | # Override methods 48 | def register_view(self): 49 | ... 50 | 51 | # Extend methods 52 | def password_validator(self): 53 | super(CustomUserManager, self).password_validator() 54 | ... 55 | 56 | In v1.0, almost all ``UserManager`` initiation parameters have been obsolted and 57 | now only accepts the following: 58 | 59 | - Required parameters ``app``, ``db`` and ``UserClass``. 60 | - Optional keyword parameters: ``UserEmailClass`` and ``UserInvitationClass``. 61 | 62 | .. seealso:: :ref:`UserManagerClass` 63 | 64 | Data-model changes 65 | ------------------ 66 | | The **email_confirmed_at** property has been renamed. 67 | | See :doc:`porting_basics` for porting steps and :doc:`porting_advanced` for an advanced option. 68 | 69 | | The **UserAuth class** has been deprecated. 70 | | Support for the optional v0.6 ``UserAuth`` class has been dropped in v1.0+ to simplify the Flask-User source code 71 | and make it more readable and easier to customize. 72 | 73 | See :doc:`porting_advanced` for a workaround if you can not merge the UserAuth and User classes. 74 | 75 | | The **UserInvitation class** has been renamed. 76 | | The v0.6 ``UserInvite`` class has been renamed to ``UserInvitation`` in v1.0+ 77 | to reflect that it's an object and not an action. 78 | 79 | Use the following approach to use the new class name while keeping the old table name:: 80 | 81 | class UserInvitation(db.Model): 82 | __tablename__ = 'user_invite' 83 | ... 84 | 85 | Password method changes 86 | ----------------------- 87 | | We changed the ``verify_password()`` API 88 | | from v0.6 ``verify_password(password, user)`` 89 | | to v1.0+ ``verify_password(password, password_hash)`` 90 | | to keep data-model knowledge out of the PasswordManager. 91 | 92 | Please change:: 93 | 94 | user_manager.verify_password(password, user) 95 | 96 | into:: 97 | 98 | verify_password(password, user.password) 99 | 100 | 101 | @confirm_email_required decorator deprecated 102 | -------------------------------------------- 103 | The ``@confirm_email_required`` view decorator has been deprecated for security reasons. 104 | 105 | In v0.6, the ``USER_ENABLE_LOGIN_WITHOUT_CONFIRM_EMAIL`` setting removed 106 | confirmed email protection for all the views and required developers to re-protect 107 | the vulnerable views with ``@confirm_email_required``. 108 | 109 | In v1.0+ we adopt the opposite approach where the (renamed) ``USER_ALLOW_LOGIN_WITHOUT_CONFIRMED_EMAIL=True`` 110 | setting continues to protect all the views, except those decorated with the 111 | new ``@allow_unconfirmed_email`` decorator. 112 | 113 | 114 | Contact us 115 | ---------- 116 | We believe this concludes the Porting steps for for applications with a high degree of 117 | Flask-User customizations. 118 | 119 | If, after reading :doc:`porting_advanced`, 120 | you still think this page is incomplete, please email ling.thio@gmail.com. 121 | 122 | -------- 123 | 124 | .. include:: includes/porting_submenu.rst 125 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | QuickStart 2 | ========== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/quickstart_submenu.rst 5 | 6 | -------- 7 | 8 | With less than a dozen lines of code, we can extend existing Flask applications 9 | with the following additional functionality: 10 | 11 | * User registration with username and/or email 12 | * Email confirmation 13 | * User authentication (Login and Logout) 14 | * Change username 15 | * Change password 16 | * Forgot password 17 | 18 | Choose a QuickStart app 19 | ----------------------- 20 | 21 | - :doc:`quickstart_app` -- Login with username. No need to configure SMTP. 22 | - :doc:`basic_app` -- Login with email, Role-based authentication, and Translations. 23 | - :doc:`mongodb_app` -- QuickStart App for MongoDB. 24 | 25 | Flask-User-starter-app 26 | ---------------------- 27 | 28 | While the example apps provide a quick way to illustrate the use of Flask-User, 29 | we do not recommend its single-file techniques. 30 | 31 | The Flask-User-starter-app follows typical Flask application practices using multiple files 32 | in an organized directory structure:: 33 | 34 | app/ 35 | commands/ 36 | models/ 37 | static/ 38 | templates/ 39 | views/ 40 | tests/ 41 | 42 | See https://github.com/lingthio/Flask-User-starter-app 43 | 44 | This may serve as a great starting place to create your next Flask application. 45 | 46 | -------- 47 | 48 | .. include:: includes/submenu_defs.rst 49 | .. include:: includes/quickstart_submenu.rst 50 | -------------------------------------------------------------------------------- /docs/source/quickstart_app.rst: -------------------------------------------------------------------------------- 1 | QuickStart App 2 | ============== 3 | .. include:: includes/submenu_defs.rst 4 | .. include:: includes/quickstart_submenu.rst 5 | 6 | -------- 7 | 8 | The QuickStart App allows users to register and login with a username 9 | and avoids the need to configure SMTP settings. 10 | 11 | Create a development environment 12 | -------------------------------- 13 | We recommend making use of virtualenv and virtualenvwrapper:: 14 | 15 | # Create virtual env 16 | mkvirtualenv my_app 17 | workon my_app 18 | 19 | # Create working directory 20 | mkdir -p ~dev/my_app # or mkdir C:\dev\my_app 21 | cd ~/dev/my_app # or cd C:\dev\my_app 22 | 23 | Install required Python packages 24 | -------------------------------- 25 | :: 26 | 27 | pip install Flask-User 28 | 29 | Create the quickstart_app.py file 30 | --------------------------------- 31 | 32 | - Open your favorite editor, 33 | - Copy the example below, and 34 | - Save it as ``~/dev/my_app/quickstart_app.py`` 35 | 36 | .. literalinclude:: ../../example_apps/quickstart_app.py 37 | :language: python 38 | :linenos: 39 | :emphasize-lines: 9, 23-27, 41-42, 60-61, 81 40 | 41 | Highlighted lines shows the few additional Flask-User code lines. 42 | 43 | 44 | Run the QuickStart App 45 | ---------------------- 46 | Run the QuickStart App with the following command:: 47 | 48 | cd ~/dev/my_app 49 | python quickstart_app.py 50 | 51 | And point your browser to ``http://localhost:5000``. 52 | 53 | 54 | Troubleshooting 55 | --------------- 56 | 57 | If you receive a SQLAlchemy error message, delete the quickstart_app.sqlite file and restart the app. 58 | You may be using an old DB schema in that file. 59 | 60 | -------- 61 | 62 | .. include:: includes/submenu_defs.rst 63 | .. include:: includes/quickstart_submenu.rst 64 | -------------------------------------------------------------------------------- /docs/source/recipes.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Recipes 3 | ======= 4 | 5 | Here we explain the use of Flask-User through code recipes. 6 | 7 | Login Form and Register Form on one page 8 | ---------------------------------------- 9 | Some websites may prefer to show the login form and the register form on one page. 10 | 11 | Flask-User (v0.4.9 and up) ships with a ``login_or_register.html`` form template which requires the following 12 | application config settings: 13 | 14 | * ``USER_LOGIN_TEMPLATE='flask_user/login_or_register.html'`` 15 | * ``USER_REGISTER_TEMPLATE='flask_user/login_or_register.html'`` 16 | 17 | This would accomplish the following: 18 | 19 | * The ``/user/login`` and ``user/register`` URLs will now render ``login_or_register.html``. 20 | * ``login_or_register.html`` now displays a Login form and a Register form. 21 | * The Login button will post to ``/user/login`` 22 | * The Register button will post to ``/user/register`` 23 | 24 | 25 | After registration hook 26 | ----------------------- 27 | Some applications require code to execute just after a new user registered for a new account. 28 | This can be achieved by subscribing to the ``user_registered`` signal as follows: 29 | 30 | :: 31 | 32 | from flask_user.signals import user_registered 33 | 34 | @user_registered.connect_via(app) 35 | def _after_registration_hook(sender, user, **extra): 36 | sender.logger.info('user registered') 37 | 38 | See also: :doc:`signals` 39 | 40 | 41 | Hashing Passwords 42 | ----------------- 43 | If you want to populate your database with User records with hashed passwords use ``user_manager.hash_password()``: 44 | 45 | :: 46 | 47 | user = User( 48 | email='user1@example.com', 49 | password=user_manager.hash_password('Password1'), 50 | ) 51 | db.session.add(user) 52 | db.session.commit() 53 | 54 | You can verify a password with ``user_manager.verify_password()``: 55 | 56 | :: 57 | 58 | does_match = user_manager.verify_password(password, user.password) 59 | 60 | Account Tracking 61 | ---------------- 62 | Flask-User deliberately stayed away from implementing account tracking features because: 63 | 64 | * What to track is often customer specific 65 | * Where to store it is often customer specific 66 | * Custom tracking is easy to implement using signals 67 | 68 | Here's an example of tracking login_count and last_login_ip: 69 | 70 | :: 71 | 72 | # This code has not been tested 73 | 74 | from flask import request 75 | from flask_user.signals import user_logged_in 76 | 77 | @user_logged_in.connect_via(app) 78 | def _track_logins(sender, user, **extra): 79 | user.login_count += 1 80 | user.last_login_ip = request.remote_addr 81 | db.session.commit() 82 | 83 | -------------------------------------------------------------------------------- /docs/source/signals.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Signals (event hooking) 3 | ======================= 4 | 5 | Flask Applications that want to be kept informed about certain actions that took place 6 | in the underlying Flask-User extensions can do so by subscribing 'signals' to receive 7 | event notification. 8 | 9 | Flask-User defines the following signals: 10 | 11 | .. include:: includes/signals.txt 12 | 13 | 14 | See the http://flask.pocoo.org/docs/signals/ 15 | 16 | Installing Blinker -- REQUIRED! 17 | ------------------------------- 18 | NB: Flask-User relies on Flask signals, which relies on the 'blinker' package. 19 | Event notification WILL NOT WORK without first installing the 'blinker' package. 20 | 21 | :: 22 | 23 | pip install blinker 24 | 25 | See http://pythonhosted.org/blinker/ 26 | 27 | 28 | Subscribing to Signals 29 | ---------------------- 30 | 31 | AFTER BLINKER HAS BEEN INSTALLED, An application can receive event notifications 32 | by using the event signal's ``connect_via()`` decorator:: 33 | 34 | from flask_user import user_logged_in 35 | 36 | @user_logged_in.connect_via(app) 37 | def _after_login_hook(sender, user, **extra): 38 | sender.logger.info('user logged in') 39 | 40 | | For all Flask-User event signals: 41 | | - ``sender`` points to the app, and 42 | | - ``user`` points to the user that is associated with this event. 43 | 44 | See `Subscribing to signals `_ 45 | 46 | Troubleshooting 47 | --------------- 48 | If the code looks right, but the tracking functions are not called, make sure to check 49 | to see if the 'blinker' package has been installed (analyze the output of ``pip freeze``). 50 | -------------------------------------------------------------------------------- /example_apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lingthio/Flask-User/5c652e6479036c3d33aa1626524e4e65bd3b961e/example_apps/__init__.py -------------------------------------------------------------------------------- /example_apps/auth0_app.py: -------------------------------------------------------------------------------- 1 | # This file contains an example Flask-User application. 2 | # To keep the example simple, we are applying some unusual techniques: 3 | # - Placing everything in one file 4 | # - Using class-based configuration (instead of file-based configuration) 5 | # - Using string-based templates (instead of file-based templates) 6 | 7 | from flask import Flask, render_template_string 8 | from flask_sqlalchemy import SQLAlchemy 9 | from flask_user import login_required, UserManager, UserMixin 10 | 11 | 12 | # Class-based application configuration 13 | class ConfigClass(object): 14 | """ Flask application config """ 15 | 16 | # Flask settings 17 | SECRET_KEY = 'This is an INSECURE secret!! DO NOT use this in production!!' 18 | 19 | # Flask-SQLAlchemy settings 20 | SQLALCHEMY_DATABASE_URI = 'sqlite:///quickstart_app.sqlite' # File-based SQL database 21 | SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning 22 | 23 | # Flask-User settings 24 | USER_APP_NAME = "Flask-User Auth0 App" # Shown in and email templates and page footers 25 | USER_ENABLE_AUTH0 = True 26 | USER_ENABLE_EMAIL = False 27 | 28 | # Auth0 settings 29 | AUTH0_DOMAIN = 'registersimply.auth0.com' 30 | AUTH0_CLIENT_ID = 'cUZu09_2K64imPfdl01pOWtS029FA8FF' 31 | AUTH0_CLIENT_SECRET = 'I59Hlle1CL8i1DHJtbKIFJnw8_fuizH7HUOtnB70vCg4OtfjMUjqf5Y8y1U1N4fK' 32 | 33 | def create_app(): 34 | """ Flask application factory """ 35 | 36 | # Create Flask app load app.config 37 | app = Flask(__name__) 38 | app.config.from_object(__name__+'.ConfigClass') 39 | 40 | # Initialize Flask-SQLAlchemy 41 | db = SQLAlchemy(app) 42 | 43 | # Define the User data-model. 44 | # NB: Make sure to add flask_user UserMixin !!! 45 | class User(db.Model, UserMixin): 46 | __tablename__ = 'users' 47 | id = db.Column(db.Integer, primary_key=True) 48 | active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') 49 | 50 | # User authentication information. The collation='NOCASE' is required 51 | # to search case insensitively when USER_IFIND_MODE is 'nocase_collation'. 52 | email = db.Column(db.String(255, collation='NOCASE'), nullable=False, unique=True) 53 | 54 | # User information 55 | first_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 56 | last_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 57 | 58 | # Create all database tables 59 | db.create_all() 60 | 61 | # Setup Flask-User and specify the User data-model 62 | user_manager = UserManager(app, db, User) 63 | 64 | # The Home page is accessible to anyone 65 | @app.route('/') 66 | def home_page(): 67 | # String-based templates 68 | return render_template_string(""" 69 | {% extends "flask_user_layout.html" %} 70 | {% block content %} 71 |

Home page

72 |

Register

73 |

Sign in

74 |

Home page (accessible to anyone)

75 |

Member page (login required)

76 |

Sign out

77 | {% endblock %} 78 | """) 79 | 80 | # The Members page is only accessible to authenticated users via the @login_required decorator 81 | @app.route('/members') 82 | @login_required # User must be authenticated 83 | def member_page(): 84 | # String-based templates 85 | return render_template_string(""" 86 | {% extends "flask_user_layout.html" %} 87 | {% block content %} 88 |

Members page

89 |

Register

90 |

Sign in

91 |

Home page (accessible to anyone)

92 |

Member page (login required)

93 |

Sign out

94 | {% endblock %} 95 | """) 96 | 97 | return app 98 | 99 | 100 | # Start development web server 101 | if __name__=='__main__': 102 | app = create_app() 103 | app.run(host='0.0.0.0', port=5000, debug=True) 104 | 105 | -------------------------------------------------------------------------------- /example_apps/dynamodb_app.py: -------------------------------------------------------------------------------- 1 | # This file contains an example Flask-User application. 2 | # To keep the example simple, we are applying some unusual techniques: 3 | # - Placing everything in one file 4 | # - Using class-based configuration (instead of file-based configuration) 5 | # - Using string-based templates (instead of file-based templates) 6 | 7 | import os 8 | from flask import Flask, render_template_string 9 | from flask_mail import Mail 10 | from flask_user import login_required, UserManager, UserMixin 11 | from flywheel import Model, Field, GlobalIndex 12 | from flask_flywheel import Flywheel 13 | from datetime import datetime 14 | import uuid 15 | 16 | 17 | # Use a Class-based config to avoid needing a 2nd file 18 | # os.getenv() enables configuration through OS environment variables 19 | class ConfigClass(object): 20 | # Flask settings 21 | SECRET_KEY = os.getenv('SECRET_KEY', 'THIS IS AN INSECURE SECRET') 22 | CSRF_ENABLED = True 23 | 24 | # Flask-Mail settings 25 | MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'email@example.com') 26 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', 'password') 27 | MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', '"MyApp" ') 28 | MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.gmail.com') 29 | MAIL_PORT = int(os.getenv('MAIL_PORT', '465')) 30 | MAIL_USE_SSL = int(os.getenv('MAIL_USE_SSL', True)) 31 | 32 | # Flask-User settings 33 | USER_APP_NAME = "AppName" # Used by email templates 34 | 35 | 36 | def create_app(): 37 | """ Flask application factory """ 38 | 39 | # Setup Flask app and app.config 40 | app = Flask(__name__) 41 | app.config.from_object(__name__ + '.ConfigClass') 42 | 43 | # Initialize Flask extensions 44 | db = Flywheel(app) # Initialize Flask-Flywheel 45 | mail = Mail(app) # Initialize Flask-Mail 46 | 47 | # Define the User data model. Make sure to add flask_user UserMixin !!! 48 | class User(Model, UserMixin): 49 | __metadata__ = { 50 | "_name": "users", 51 | 'throughput': { 52 | 'read': 1, 53 | 'write': 1, 54 | }, 55 | 'global_indexes': [ 56 | GlobalIndex.all('email-username', 'username').throughput(read=1, write=1), 57 | GlobalIndex.all('email-index', 'email').throughput(read=1, write=1) 58 | ], 59 | } 60 | 61 | id = Field(hash_key=True) 62 | active = Field(data_type=bool) 63 | 64 | # User authentication information 65 | username = Field() 66 | password = Field() 67 | 68 | # User email information 69 | email = Field() 70 | email_confirmed_at = Field(data_type=datetime) 71 | 72 | # User information 73 | first_name = Field() 74 | last_name = Field() 75 | 76 | def get_id(self): 77 | if self.id is None: 78 | self.id = str(uuid.uuid1()) 79 | return self.id 80 | 81 | # Setup Flask-User 82 | user_manager = UserManager(app, db, User) 83 | 84 | # Create all database tables 85 | db.engine.register(User) 86 | print('create_schema()') 87 | db.engine.create_schema() 88 | print('created_schema()') 89 | 90 | # The Home page is accessible to anyone 91 | @app.route('/') 92 | def home_page(): 93 | return render_template_string(""" 94 | {% extends "base.html" %} 95 | {% block content %} 96 |

Home page

97 |

This page can be accessed by anyone.


98 |

Home page (anyone)

99 |

Members page (login required)

100 | {% endblock %} 101 | """) 102 | 103 | # The Members page is only accessible to authenticated users 104 | @app.route('/members') 105 | @login_required # Use of @login_required decorator 106 | def members_page(): 107 | return render_template_string(""" 108 | {% extends "base.html" %} 109 | {% block content %} 110 |

Members page

111 |

This page can only be accessed by authenticated users.


112 |

Home page (anyone)

113 |

Members page (login required)

114 | {% endblock %} 115 | """) 116 | 117 | return app 118 | 119 | 120 | # Start development web server 121 | app = create_app() 122 | if __name__ == '__main__': 123 | app.run(host='0.0.0.0', port=5000, debug=True) 124 | 125 | -------------------------------------------------------------------------------- /example_apps/mongodb_app.py: -------------------------------------------------------------------------------- 1 | # This file contains an example Flask-User application. 2 | # To keep the example simple, we are applying some unusual techniques: 3 | # - Placing everything in one file 4 | # - Using class-based configuration (instead of file-based configuration) 5 | # - Using string-based templates (instead of file-based templates) 6 | 7 | from flask import Flask, render_template_string 8 | from flask_mongoengine import MongoEngine 9 | from flask_user import login_required, UserManager, UserMixin 10 | 11 | 12 | # Class-based application configuration 13 | class ConfigClass(object): 14 | """ Flask application config """ 15 | 16 | # Flask settings 17 | SECRET_KEY = 'This is an INSECURE secret!! DO NOT use this in production!!' 18 | 19 | # Flask-MongoEngine settings 20 | MONGODB_SETTINGS = { 21 | 'db': 'tst_app', 22 | 'host': 'mongodb://localhost:27017/tst_app' 23 | } 24 | 25 | # Flask-User settings 26 | USER_APP_NAME = "Flask-User MongoDB App" # Shown in and email templates and page footers 27 | USER_ENABLE_EMAIL = False # Disable email authentication 28 | USER_ENABLE_USERNAME = True # Enable username authentication 29 | USER_REQUIRE_RETYPE_PASSWORD = False # Simplify register form 30 | 31 | 32 | def create_app(): 33 | """ Flask application factory """ 34 | 35 | # Setup Flask and load app.config 36 | app = Flask(__name__) 37 | app.config.from_object(__name__+'.ConfigClass') 38 | 39 | # Setup Flask-MongoEngine 40 | db = MongoEngine(app) 41 | 42 | # Define the User document. 43 | # NB: Make sure to add flask_user UserMixin !!! 44 | class User(db.Document, UserMixin): 45 | active = db.BooleanField(default=True) 46 | 47 | # User authentication information 48 | username = db.StringField(default='') 49 | password = db.StringField() 50 | 51 | # User information 52 | first_name = db.StringField(default='') 53 | last_name = db.StringField(default='') 54 | 55 | # Relationships 56 | roles = db.ListField(db.StringField(), default=[]) 57 | 58 | # Setup Flask-User and specify the User data-model 59 | user_manager = UserManager(app, db, User) 60 | 61 | # The Home page is accessible to anyone 62 | @app.route('/') 63 | def home_page(): 64 | # String-based templates 65 | return render_template_string(""" 66 | {% extends "flask_user_layout.html" %} 67 | {% block content %} 68 |

Home page

69 |

Register

70 |

Sign in

71 |

Home page (accessible to anyone)

72 |

Member page (login required)

73 |

Sign out

74 | {% endblock %} 75 | """) 76 | 77 | # The Members page is only accessible to authenticated users via the @login_required decorator 78 | @app.route('/members') 79 | @login_required # User must be authenticated 80 | def member_page(): 81 | # String-based templates 82 | return render_template_string(""" 83 | {% extends "flask_user_layout.html" %} 84 | {% block content %} 85 |

Members page

86 |

Register

87 |

Sign in

88 |

Home page (accessible to anyone)

89 |

Member page (login required)

90 |

Sign out

91 | {% endblock %} 92 | """) 93 | 94 | return app 95 | 96 | 97 | # Start development web server 98 | if __name__=='__main__': 99 | app = create_app() 100 | app.run(host='0.0.0.0', port=5000, debug=True) 101 | 102 | -------------------------------------------------------------------------------- /example_apps/multi_email_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template_string 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_user import login_required, UserManager, UserMixin 5 | 6 | 7 | # Use a Class-based config to avoid needing a 2nd file 8 | # os.getenv() enables configuration through OS environment variables 9 | class ConfigClass(object): 10 | # Flask settings 11 | SECRET_KEY = 'This is an INSECURE secret!! DO NOT use this in production!!' 12 | SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///multi_email_app.sqlite') 13 | CSRF_ENABLED = True 14 | 15 | # Flask-SQLAlchemy settings 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | 18 | # Flask-Mail settings 19 | MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'email@example.com') 20 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', 'password') 21 | MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', '"MyApp" ') 22 | MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.gmail.com') 23 | MAIL_PORT = int(os.getenv('MAIL_PORT', '465')) 24 | MAIL_USE_SSL = int(os.getenv('MAIL_USE_SSL', True)) 25 | 26 | # Flask-User settings 27 | USER_APP_NAME = "AppName" # Used by email templates 28 | USER_ENABLE_MULTIPLE_EMAILS = True 29 | 30 | 31 | def create_app(): 32 | """ Flask application factory """ 33 | 34 | # Setup Flask app and app.config 35 | app = Flask(__name__) 36 | app.config.from_object(__name__+'.ConfigClass') 37 | 38 | # Initialize Flask extensions 39 | db = SQLAlchemy(app) # Initialize Flask-SQLAlchemy 40 | 41 | # Define the User data-model. Make sure to add flask_user UserMixin !!! 42 | class User(db.Model, UserMixin): 43 | __tablename__ = 'users' 44 | id = db.Column(db.Integer, primary_key=True) 45 | 46 | # User authentication information 47 | username = db.Column(db.String(50, collation='NOCASE'), nullable=False, unique=True) 48 | password = db.Column(db.String(255), nullable=False, server_default='') 49 | 50 | # User information 51 | active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0') 52 | first_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 53 | last_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 54 | 55 | # Relationship 56 | user_emails = db.relationship('UserEmail') 57 | 58 | 59 | # Define UserEmail DataModel. 60 | class UserEmail(db.Model): 61 | __tablename__ = 'user_emails' 62 | id = db.Column(db.Integer, primary_key=True) 63 | user_id = db.Column(db.Integer, db.ForeignKey('users.id')) 64 | 65 | # User email information 66 | email = db.Column(db.String(255, collation='NOCASE'), nullable=False, unique=True) 67 | email_confirmed_at = db.Column(db.DateTime()) 68 | is_primary = db.Column(db.Boolean(), nullable=False, default=False) 69 | 70 | # Relationship 71 | user = db.relationship('User', uselist=False) 72 | 73 | 74 | # Create all database tables 75 | db.create_all() 76 | 77 | # Setup Flask-User 78 | db_adapter = SQLAlchemyAdapter(db, User, UserEmailClass=UserEmail) # Register the User data-model 79 | user_manager = UserManager(db_adapter, app) # Initialize Flask-User 80 | 81 | # The Home page is accessible to anyone 82 | @app.route('/') 83 | def home_page(): 84 | return render_template_string(""" 85 | {% extends "flask_user_layout.html" %} 86 | {% block content %} 87 |

Home page

88 |

This page can be accessed by anyone.


89 |

Home page (anyone)

90 |

Members page (login required)

91 | {% endblock %} 92 | """) 93 | 94 | # The Members page is only accessible to authenticated users 95 | @app.route('/members') 96 | @login_required # Use of @login_required decorator 97 | def member_page(): 98 | return render_template_string(""" 99 | {% extends "flask_user_layout.html" %} 100 | {% block content %} 101 |

Members page

102 |

This page can only be accessed by authenticated users.


103 |

Home page (anyone)

104 |

Members page (login required)

105 | {% endblock %} 106 | """) 107 | 108 | return app 109 | 110 | 111 | # Start development web server 112 | if __name__=='__main__': 113 | app = create_app() 114 | app.run(host='0.0.0.0', port=5000, debug=True) 115 | 116 | -------------------------------------------------------------------------------- /example_apps/pynamodb_app.py: -------------------------------------------------------------------------------- 1 | # This file contains an example Flask-User application. 2 | # To keep the example simple, we are applying some unusual techniques: 3 | # - Placing everything in one file 4 | # - Using class-based configuration (instead of file-based configuration) 5 | # - Using string-based templates (instead of file-based templates) 6 | 7 | import os 8 | import uuid 9 | 10 | from flask import Flask, render_template_string 11 | from flask_mail import Mail 12 | from flask_user import login_required, UserManager, UserMixin 13 | from pynamodb.models import Model 14 | from pynamodb.indexes import GlobalSecondaryIndex, AllProjection 15 | from pynamodb.attributes import UnicodeAttribute, BooleanAttribute, UTCDateTimeAttribute 16 | 17 | 18 | # Use a Class-based config to avoid needing a 2nd file 19 | # os.getenv() enables configuration through OS environment variables 20 | class ConfigClass(object): 21 | # Flask settings 22 | SECRET_KEY = os.getenv('SECRET_KEY', 'THIS IS AN INSECURE SECRET') 23 | CSRF_ENABLED = True 24 | 25 | # Flask-Mail settings 26 | MAIL_USERNAME = os.getenv('MAIL_USERNAME', 'email@example.com') 27 | MAIL_PASSWORD = os.getenv('MAIL_PASSWORD', 'password') 28 | MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER', '"MyApp" ') 29 | MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.gmail.com') 30 | MAIL_PORT = int(os.getenv('MAIL_PORT', '465')) 31 | MAIL_USE_SSL = int(os.getenv('MAIL_USE_SSL', True)) 32 | 33 | # Flask-User settings 34 | USER_APP_NAME = "AppName" # Used by email templates 35 | 36 | 37 | def create_app(): 38 | """ Flask application factory """ 39 | 40 | # Setup Flask app and app.config 41 | app = Flask(__name__) 42 | app.config.from_object(__name__ + '.ConfigClass') 43 | 44 | # Initialize Flask extensions 45 | db = None 46 | mail = Mail(app) # Initialize Flask-Mail 47 | 48 | # Define the User data model. Make sure to add flask_user UserMixin !!! 49 | class UsernameIndex(GlobalSecondaryIndex): 50 | class Meta: 51 | read_capacity_units = 1 52 | write_capacity_units = 1 53 | projection = AllProjection() 54 | 55 | username = UnicodeAttribute(hash_key=True) 56 | 57 | class EmailIndex(GlobalSecondaryIndex): 58 | class Meta: 59 | read_capacity_units = 1 60 | write_capacity_units = 1 61 | projection = AllProjection() 62 | 63 | email = UnicodeAttribute(hash_key=True) 64 | 65 | class User(Model, UserMixin): 66 | class Meta: 67 | table_name = 'users' 68 | 69 | id = UnicodeAttribute(hash_key=True, default=lambda: str(uuid.uuid1())) 70 | active = BooleanAttribute() 71 | 72 | # User authentication information 73 | username = UnicodeAttribute(null=True) 74 | password = UnicodeAttribute(null=True) 75 | 76 | username_index = UsernameIndex() 77 | 78 | # User email information 79 | email = UnicodeAttribute(null=True) 80 | email_confirmed_at = UTCDateTimeAttribute(null=True) 81 | 82 | email_index = EmailIndex() 83 | 84 | # User information 85 | first_name = UnicodeAttribute(null=True) 86 | last_name = UnicodeAttribute(null=True) 87 | 88 | 89 | # Setup Flask-User 90 | user_manager = UserManager(app, db, User) 91 | 92 | # Create all database tables 93 | print('create_schema()') 94 | user_manager.db_manager.create_all_tables() 95 | print('created_schema()') 96 | 97 | # The Home page is accessible to anyone 98 | @app.route('/') 99 | def home_page(): 100 | return render_template_string(""" 101 | {% block content %} 102 |

Home page

103 |

This page can be accessed by anyone.


104 |

Home page (anyone)

105 |

Members page (login required)

106 | {% endblock %} 107 | """) 108 | 109 | # The Members page is only accessible to authenticated users 110 | @app.route('/members') 111 | @login_required # Use of @login_required decorator 112 | def members_page(): 113 | return render_template_string(""" 114 | {% block content %} 115 |

Members page

116 |

This page can only be accessed by authenticated users.


117 |

Home page (anyone)

118 |

Members page (login required)

119 | {% endblock %} 120 | """) 121 | 122 | return app 123 | 124 | 125 | # Start development web server 126 | app = create_app() 127 | if __name__ == '__main__': 128 | app.run(host='0.0.0.0', port=5000, debug=True) 129 | -------------------------------------------------------------------------------- /example_apps/quickstart_app.py: -------------------------------------------------------------------------------- 1 | # This file contains an example Flask-User application. 2 | # To keep the example simple, we are applying some unusual techniques: 3 | # - Placing everything in one file 4 | # - Using class-based configuration (instead of file-based configuration) 5 | # - Using string-based templates (instead of file-based templates) 6 | 7 | from flask import Flask, render_template_string 8 | from flask_sqlalchemy import SQLAlchemy 9 | from flask_user import login_required, UserManager, UserMixin 10 | 11 | 12 | # Class-based application configuration 13 | class ConfigClass(object): 14 | """ Flask application config """ 15 | 16 | # Flask settings 17 | SECRET_KEY = 'This is an INSECURE secret!! DO NOT use this in production!!' 18 | 19 | # Flask-SQLAlchemy settings 20 | SQLALCHEMY_DATABASE_URI = 'sqlite:///quickstart_app.sqlite' # File-based SQL database 21 | SQLALCHEMY_TRACK_MODIFICATIONS = False # Avoids SQLAlchemy warning 22 | 23 | # Flask-User settings 24 | USER_APP_NAME = "Flask-User QuickStart App" # Shown in and email templates and page footers 25 | USER_ENABLE_EMAIL = False # Disable email authentication 26 | USER_ENABLE_USERNAME = True # Enable username authentication 27 | USER_REQUIRE_RETYPE_PASSWORD = False # Simplify register form 28 | 29 | 30 | def create_app(): 31 | """ Flask application factory """ 32 | 33 | # Create Flask app load app.config 34 | app = Flask(__name__) 35 | app.config.from_object(__name__+'.ConfigClass') 36 | 37 | # Initialize Flask-SQLAlchemy 38 | db = SQLAlchemy(app) 39 | 40 | # Define the User data-model. 41 | # NB: Make sure to add flask_user UserMixin !!! 42 | class User(db.Model, UserMixin): 43 | __tablename__ = 'users' 44 | id = db.Column(db.Integer, primary_key=True) 45 | active = db.Column('is_active', db.Boolean(), nullable=False, server_default='1') 46 | 47 | # User authentication information. The collation='NOCASE' is required 48 | # to search case insensitively when USER_IFIND_MODE is 'nocase_collation'. 49 | username = db.Column(db.String(100, collation='NOCASE'), nullable=False, unique=True) 50 | password = db.Column(db.String(255), nullable=False, server_default='') 51 | email_confirmed_at = db.Column(db.DateTime()) 52 | 53 | # User information 54 | first_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 55 | last_name = db.Column(db.String(100, collation='NOCASE'), nullable=False, server_default='') 56 | 57 | # Create all database tables 58 | db.create_all() 59 | 60 | # Setup Flask-User and specify the User data-model 61 | user_manager = UserManager(app, db, User) 62 | 63 | # The Home page is accessible to anyone 64 | @app.route('/') 65 | def home_page(): 66 | # String-based templates 67 | return render_template_string(""" 68 | {% extends "flask_user_layout.html" %} 69 | {% block content %} 70 |

Home page

71 |

Register

72 |

Sign in

73 |

Home page (accessible to anyone)

74 |

Member page (login required)

75 |

Sign out

76 | {% endblock %} 77 | """) 78 | 79 | # The Members page is only accessible to authenticated users via the @login_required decorator 80 | @app.route('/members') 81 | @login_required # User must be authenticated 82 | def member_page(): 83 | # String-based templates 84 | return render_template_string(""" 85 | {% extends "flask_user_layout.html" %} 86 | {% block content %} 87 |

Members page

88 |

Register

89 |

Sign in

90 |

Home page (accessible to anyone)

91 |

Member page (login required)

92 |

Sign out

93 | {% endblock %} 94 | """) 95 | 96 | return app 97 | 98 | 99 | # Start development web server 100 | if __name__=='__main__': 101 | app = create_app() 102 | app.run(host='0.0.0.0', port=5000, debug=True) 103 | 104 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | from fabric.api import task 2 | from fabric.operations import local 3 | 4 | 5 | @task 6 | def runserver(): 7 | local('python runserver.py') 8 | 9 | @task 10 | def runapp(appname): 11 | local('PYTHONPATH=. python example_apps/'+appname+'.py') 12 | 13 | @task 14 | def babel(command): 15 | # Generate the .pot file from source code files 16 | if command=='extract': 17 | local('pybabel extract -F flask_user/translations/babel.cfg -k lazy_gettext -c NOTE -o flask_user/translations/flask_user.pot --project Flask-User --version v1.0.0.0 flask_user flask_user') 18 | 19 | # Update .po files from the .pot file 20 | elif command=='update': 21 | local('pybabel update -i flask_user/translations/flask_user.pot --domain=flask_user --output-dir flask_user/translations') 22 | elif command=='compile': 23 | local('pybabel compile -f --domain=flask_user --directory flask_user/translations') 24 | 25 | @task 26 | def test(): 27 | # Requires "pip install pytest" 28 | local('py.test flask_user/tests/') 29 | 30 | @task 31 | def cov(): 32 | # Requires "pip install pytest-coverage" 33 | local('py.test --cov flask_user --cov-report term-missing --cov-config flask_user/tests/.coveragerc flask_user/tests/') 34 | 35 | @task 36 | def cov2(): 37 | # Requires "pip install pytest-coverage" 38 | local('py.test --cov flask_user --cov-report term-missing --cov-config flask_user/tests/.coveragerc flask_user/tests/test_views.py') 39 | 40 | @task 41 | def profiling(): 42 | # Requires "pip install pytest-profiling" 43 | local('py.test --profile flask_user/tests/') 44 | 45 | 46 | @task 47 | def docs(rebuild=False): 48 | # local('cp example_apps/*_app.py docs/source/includes/.') 49 | options='' 50 | if rebuild: 51 | options += ' -E' 52 | local('sphinx-build -b html -a {options} docs/source ../builds/flask_user1/docs'.format(options=options)) 53 | local('cd ../builds/flask_user1/docs && zip -u -r flask_user1_docs *') 54 | 55 | # sphinx-apidoc -f -o docs/source flask_user flask_user/tests flask_user/db_adapters 56 | # rm docs/source/flask_user.rst docs/source/modules.rst 57 | 58 | # PyEnv: https://gist.github.com/Bouke/11261620 59 | # PyEnv and Tox: https://www.holger-peters.de/using-pyenv-and-tox.html 60 | # Available Python versions: pyenv install --list 61 | @task 62 | def setup_tox(): 63 | versions_str = '2.7.17 3.4.10 3.5.9 3.6.9 3.7.5 3.8.0' 64 | versions = versions_str.split() 65 | for version in versions: 66 | local('pyenv install --skip-existing '+version) 67 | local('pyenv local '+versions_str) 68 | 69 | @task 70 | def tox(): 71 | local('tox') 72 | 73 | @task 74 | def start_mongodb(): 75 | local('mongod -dbpath ~/mongodb/data/db') 76 | 77 | @task 78 | def build_dist(): 79 | # Compile translation files 80 | babel('compile') 81 | # Build distribution file 82 | local('rm -f dist/*') 83 | local('python setup.py sdist') 84 | 85 | @task 86 | def upload_to_pypi(): 87 | build_dist() 88 | local('twine upload dist/*') 89 | 90 | -------------------------------------------------------------------------------- /flask_user/README.rst: -------------------------------------------------------------------------------- 1 | Design overview 2 | =============== 3 | The Core Logic shields itself from non-core services through the use 4 | of Manager Interfaces. 5 | 6 | - The DBManager manages objects in a database. 7 | - The EmailManager sends email messages 8 | - The PasswordManager hashes and verifies passwords 9 | - The TokenManager generates and verifies tokens. 10 | 11 | The DBManager shields itself from specific database services 12 | through the use of the DbAdapter interface. 13 | Flask-User ships with the following implementations:: 14 | 15 | - DynamoDbAdapter 16 | - MongoDbAdapter 17 | - SQLDbAdapter 18 | 19 | The EmailManager shields itself from specific email services 20 | through the use of the EmailAdapter interface. 21 | Flask-User ships with the following implementations:: 22 | 23 | - SendgridEmailAdapter 24 | - SendmailEmailAdapter 25 | - SMTPEmailAdapter 26 | 27 | Because the core logic is relatively small, we decided to simply 28 | include the core logic with the DBManager. 29 | 30 | :: 31 | 32 | Views 33 | 34 | +----------------------- =================== ----------------------------+ 35 | | DBManager interface | 36 | | | 37 | | DBManager and Core Logic | 38 | | | 39 | | +--------------------------------------------------------+ 40 | | | 41 | | | ============== ================= ============== 42 | | | EmailManager PasswordManager TokenManager 43 | | | Interface Interface Interface 44 | | | +------------+ +---------------+ +------------+ 45 | | DBManager | |EmailManager| |PasswordManager| |TokenManager| 46 | +---------------+ +------------+ +---------------+ +------------+ 47 | 48 | ================= ============== 49 | DbAdapter EmailAdapter 50 | Interface Interface 51 | 52 | - DynamoDbAdapter - SendgridEmailAdapter 53 | - MongoDbAdapter - SendmailEmailAdapter 54 | - SQLDbAdapter - SMTPEmailAdapter 55 | 56 | -------------------------------------------------------------------------------- /flask_user/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'Flask-User' 2 | __description__ = 'Customizable User Authentication, User Management, and more.' 3 | __version__ = '1.0.2.3' 4 | __url__ = 'https://github.com/lingthio/Flask-User' 5 | __author__ = 'Ling Thio' 6 | __author_email__= 'ling.thio@gmail.com' 7 | __maintainer__ = 'Ling Thio' 8 | __license__ = 'MIT' 9 | __copyright__ = '(c) 2013 Ling Thio' 10 | 11 | # Define Flask-User Exceptions early on 12 | class ConfigError(Exception): 13 | pass 14 | 15 | class EmailError(Exception): 16 | pass 17 | 18 | 19 | # Export Flask-Login's current user 20 | from flask_login import current_user # pass through Flask-Login's current_user 21 | 22 | # Export v0.6 legacy classes DbAdapter and SQLAlchemyAdapter 23 | # To display an Incompatibility error message the v0.6 API is used on a v1.0+ install 24 | from .legacy_error import DbAdapter, SQLAlchemyAdapter 25 | 26 | from .user_mixin import UserMixin 27 | from .user_manager import UserManager 28 | from .email_manager import EmailManager 29 | from .password_manager import PasswordManager 30 | from .token_manager import TokenManager 31 | 32 | # Export Flask-User decorators 33 | from .decorators import * 34 | 35 | # Export Flask-User signals 36 | from .signals import * 37 | -------------------------------------------------------------------------------- /flask_user/db_adapters/__init__.py: -------------------------------------------------------------------------------- 1 | from .db_adapter_interface import DbAdapterInterface 2 | from .sql_db_adapter import SQLDbAdapter 3 | from .mongo_db_adapter import MongoDbAdapter 4 | from .dynamo_db_adapter import DynamoDbAdapter 5 | from .pynamo_db_adapter import PynamoDbAdapter 6 | -------------------------------------------------------------------------------- /flask_user/db_adapters/db_adapter_interface.py: -------------------------------------------------------------------------------- 1 | """This module defines the DbAdapter interface. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | 9 | class DbAdapterInterface(object): 10 | """ Define the DbAdapter interface to manage objects in various databases. 11 | 12 | This interface supports session-based ODMs (``db.session.add()/commit()``) 13 | as well as object-based ODMs (``object.save()``). 14 | """ 15 | 16 | def __init__(self, app, db): 17 | """ 18 | Args: 19 | app(Flask): The Flask appliation instance. 20 | db: The object-database mapper instance. 21 | """ 22 | self.app = app 23 | self.db = db 24 | self.user_manager = self.app.user_manager 25 | 26 | def add_object(self, object): 27 | """ Add a new object to the database. 28 | 29 | | Session-based ODMs would call something like ``db.session.add(object)``. 30 | | Object-based ODMs would call something like ``object.save()``. 31 | """ 32 | raise NotImplementedError 33 | 34 | def commit(self): 35 | """Save all modified session objects to the database. 36 | 37 | | Session-based ODMs would call something like ``db.session.commit()``. 38 | | Object-based ODMs would do nothing. 39 | """ 40 | raise NotImplementedError 41 | 42 | def delete_object(self, object): 43 | """ Delete object from database. 44 | """ 45 | raise NotImplementedError 46 | 47 | def find_objects(self, ObjectClass, **kwargs): 48 | """ Retrieve all objects of type ``ObjectClass``, 49 | matching the specified filters in ``**kwargs`` -- case sensitive. 50 | """ 51 | raise NotImplementedError 52 | 53 | def find_first_object(self, ObjectClass, **kwargs): 54 | """ Retrieve the first object of type ``ObjectClass``, 55 | matching the specified filters in ``**kwargs`` -- case sensitive. 56 | """ 57 | raise NotImplementedError 58 | 59 | def ifind_first_object(self, ObjectClass, **kwargs): 60 | """ Retrieve the first object of type ``ObjectClass``, 61 | matching the specified filters in ``**kwargs`` -- case insensitive. 62 | 63 | | If USER_IFIND_MODE is 'nocase_collation' this method maps to find_first_object(). 64 | | If USER_IFIND_MODE is 'ifind' this method performs a case insensitive find. 65 | """ 66 | raise NotImplementedError 67 | 68 | def get_object(self, ObjectClass, id): 69 | """ Retrieve object of type ``ObjectClass`` by ``id``. 70 | 71 | | Returns object on success. 72 | | Returns None otherwise. 73 | """ 74 | raise NotImplementedError 75 | 76 | def save_object(self, object): 77 | """ Save object to database. 78 | 79 | | Session-based ODMs would do nothing. 80 | | Object-based ODMs would do something like object.save(). 81 | """ 82 | raise NotImplementedError 83 | 84 | 85 | # Database management methods 86 | # --------------------------- 87 | 88 | def create_all_tables(self): 89 | """Create database tables for all known database data-models.""" 90 | raise NotImplementedError 91 | 92 | def drop_all_tables(self): 93 | """Drop all tables. 94 | 95 | .. warning:: ALL DATA WILL BE LOST. Use only for automated testing. 96 | """ 97 | raise NotImplementedError 98 | 99 | -------------------------------------------------------------------------------- /flask_user/db_adapters/dynamo_db_adapter.py: -------------------------------------------------------------------------------- 1 | """This module implements the DbAdapter interface for Flywheel. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | import pdb 9 | 10 | # Non-system imports are moved into the methods to make them an optional requirement 11 | 12 | from flask_user.db_adapters import DbAdapterInterface 13 | 14 | 15 | class DynamoDbAdapter(DbAdapterInterface): 16 | """ Implements the DbAdapter interface to find, add, update and delete 17 | database objects using Flask-Flywheel. 18 | """ 19 | 20 | def __init__(self, app, db): 21 | """Args: 22 | app(Flask): The Flask appliation instance. 23 | db(Flywheel): The Flywheel object-database mapper instance. 24 | 25 | | Example: 26 | | app = Flask(__name__) 27 | | db = Flywheel() 28 | | db_adapter = DynamoDbAdapter(app, db) 29 | """ 30 | # This no-op method is defined to show it in Sphinx docs in order 'bysource' 31 | super(DynamoDbAdapter, self).__init__(app, db) 32 | 33 | def add_object(self, object): 34 | """Add object to db session. Only for session-centric object-database mappers.""" 35 | if object.id is None: 36 | object.get_id() 37 | self.db.engine.save(object) 38 | 39 | def get_object(self, ObjectClass, id): 40 | """ Retrieve object of type ``ObjectClass`` by ``id``. 41 | 42 | | Returns object on success. 43 | | Returns None otherwise. 44 | """ 45 | print('dynamo.get(%s, %s)' % (ObjectClass, str(id))) 46 | resp = self.db.engine.get(ObjectClass, [id]) 47 | if resp: 48 | return resp[0] 49 | else: 50 | return None 51 | 52 | def find_objects(self, ObjectClass, **kwargs): 53 | """ Retrieve all objects of type ``ObjectClass``, 54 | matching the filters specified in ``**kwargs`` -- case sensitive. 55 | """ 56 | 57 | print('dynamo.find_objects(%s, %s)' % (ObjectClass, str(kwargs))) 58 | 59 | query = self.db.engine.query(ObjectClass) 60 | for field_name, field_value in kwargs.items(): 61 | 62 | # Make sure that ObjectClass has a 'field_name' property 63 | field = getattr(ObjectClass, field_name, None) 64 | if field is None: 65 | raise KeyError("DynamoDBAdapter.find_objects(): Class '%s' has no field '%s'." % (ObjectClass, field_name)) 66 | 67 | # Add a case sensitive filter to the query 68 | query = query.filter(field == field_value) 69 | 70 | # Execute query 71 | return query.all(desc=True) 72 | 73 | def find_first_object(self, ObjectClass, **kwargs): 74 | """ Retrieve the first object of type ``ObjectClass``, 75 | matching the filters specified in ``**kwargs`` -- case sensitive. 76 | 77 | ``find_first_object(User, username='myname')`` translates to 78 | ``User.query.filter(User.username=='myname').first()``. 79 | """ 80 | 81 | print('dynamo.find_first_object(%s, %s)' % (ObjectClass, str(kwargs))) 82 | query = self.db.engine.query(ObjectClass) 83 | for field_name, field_value in kwargs.items(): 84 | 85 | # Make sure that ObjectClass has a 'field_name' property 86 | field = getattr(ObjectClass, field_name, None) 87 | if field is None: 88 | raise KeyError("DynamoDBAdapter.find_first_object(): Class '%s' has no field '%s'." % (ObjectClass, field_name)) 89 | 90 | # Add a case sensitive filter to the query 91 | query = query.filter(field == field_value) 92 | 93 | # Execute query 94 | out = query.first(desc=True)#, attributes=['password']) 95 | return out 96 | 97 | def ifind_first_object(self, ObjectClass, **kwargs): 98 | """ Retrieve the first object of type ``ObjectClass``, 99 | matching the specified filters in ``**kwargs`` -- case insensitive. 100 | 101 | | If USER_IFIND_MODE is 'nocase_collation' this method maps to find_first_object(). 102 | | If USER_IFIND_MODE is 'ifind' this method performs a case insensitive find. 103 | """ 104 | # Call regular find() if USER_IFIND_MODE is nocase_collation 105 | if self.user_manager.USER_IFIND_MODE=='nocase_collation': 106 | return self.find_first_object(ObjectClass, **kwargs) 107 | 108 | raise NotImplementedError 109 | 110 | def save_object(self, object, **kwargs): 111 | """ Save object. Only for non-session centric Object-Database Mappers.""" 112 | self.db.engine.sync(object) 113 | 114 | def delete_object(self, object): 115 | """ Delete object specified by ``object``. """ 116 | #pdb.set_trace() 117 | self.db.engine.delete_key(object)#, userid='abc123', id='1') 118 | print('dynamo.delete_object(%s)' % object) 119 | #self.db.session.delete(object) 120 | 121 | def commit(self): 122 | """This method does nothing for DynamoDbAdapter. 123 | """ 124 | # pdb.set_trace() 125 | print('dynamo.commit()') 126 | #self.db.engine.sync() 127 | # self.db.session.commit() 128 | 129 | 130 | # Database management methods 131 | # --------------------------- 132 | 133 | def create_all_tables(self): 134 | """This method does nothing for DynamoDbAdapter.""" 135 | self.db.engine.create_schema() 136 | 137 | def drop_all_tables(self): 138 | """Drop all document collections of the database. 139 | 140 | .. warning:: ALL DATA WILL BE LOST. Use only for automated testing. 141 | """ 142 | self.db.engine.delete_schema() 143 | -------------------------------------------------------------------------------- /flask_user/db_adapters/mongo_db_adapter.py: -------------------------------------------------------------------------------- 1 | """This module implements the DbAdapter interface for MongoEngine. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | 9 | # Non-system imports are moved into the methods to make them an optional requirement 10 | 11 | from flask_user.db_adapters import DbAdapterInterface 12 | 13 | 14 | class MongoDbAdapter(DbAdapterInterface): 15 | """ Implements the DbAdapter interface to find, add, update and delete 16 | database objects using Flask-MongoEngine. 17 | """ 18 | 19 | def __init__(self, app, db): 20 | """Args: 21 | app(Flask): The Flask appliation instance. 22 | db(MongoEngine): The MongoEngine object-database mapper instance. 23 | 24 | | Example: 25 | | app = Flask(__name__) 26 | | db = MongoEngine() 27 | | db_adapter = MongoDbAdapter(app, db) 28 | """ 29 | # This no-op method is defined to show it in Sphinx docs in order 'bysource' 30 | super(MongoDbAdapter, self).__init__(app, db) 31 | 32 | def add_object(self, object): 33 | """ Add a new object to the database. 34 | 35 | | Session-based ODMs would call something like ``db.session.add(object)``. 36 | | Object-based ODMs would call something like ``object.save()``. 37 | """ 38 | object.save() 39 | 40 | def get_object(self, ObjectClass, id): 41 | """ Retrieve object of type ``ObjectClass`` by ``id``. 42 | 43 | | Returns object on success. 44 | | Returns None otherwise. 45 | """ 46 | try: 47 | object = ObjectClass.objects.get(id=id) 48 | except (ObjectClass.DoesNotExist, ObjectClass.MultipleObjectsReturned): 49 | object = None 50 | return object 51 | 52 | def find_objects(self, ObjectClass, **kwargs): 53 | """ Retrieve all objects of type ``ObjectClass``, 54 | matching the specified filters in ``**kwargs`` -- case sensitive. 55 | """ 56 | return ObjectClass.objects(**kwargs).all() 57 | 58 | def find_first_object(self, ObjectClass, **kwargs): 59 | """ Retrieve the first object of type ``ObjectClass``, 60 | matching the specified filters in ``**kwargs`` -- case sensitive. 61 | """ 62 | 63 | # Retrieve first object -- case sensitive 64 | return ObjectClass.objects(**kwargs).first() 65 | 66 | def ifind_first_object(self, ObjectClass, **kwargs): 67 | """ Retrieve the first object of type ``ObjectClass``, 68 | matching the specified filters in ``**kwargs`` -- case insensitive. 69 | 70 | | If USER_IFIND_MODE is 'nocase_collation' this method maps to find_first_object(). 71 | | If USER_IFIND_MODE is 'ifind' this method performs a case insensitive find. 72 | """ 73 | # Call regular find() if USER_IFIND_MODE is nocase_collation 74 | if self.user_manager.USER_IFIND_MODE=='nocase_collation': 75 | return self.find_first_object(ObjectClass, **kwargs) 76 | 77 | # Convert ...(email=value) to ...(email__iexact=value) 78 | iexact_kwargs = {} 79 | for key, value in kwargs.items(): 80 | iexact_kwargs[key+'__iexact'] = value 81 | # Retrieve first object -- case insensitive 82 | return ObjectClass.objects(**iexact_kwargs).first() 83 | 84 | def save_object(self, object, **kwargs): 85 | """ Save object to database. 86 | 87 | | Session-based ODMs would do nothing. 88 | | Object-based ODMs would do something like object.save(). 89 | """ 90 | object.save() 91 | 92 | def delete_object(self, object): 93 | """ Delete object from database. 94 | """ 95 | object.delete() 96 | 97 | def commit(self): 98 | """Save all modified session objects to the database. 99 | 100 | | Session-based ODMs would call something like ``db.session.commit()``. 101 | | Object-based ODMs would do nothing. 102 | """ 103 | pass 104 | 105 | 106 | # Database management methods 107 | # --------------------------- 108 | 109 | def create_all_tables(self): 110 | """This method does nothing for MongoDbAdapter.""" 111 | pass 112 | 113 | def drop_all_tables(self): 114 | """Drop all document collections of the database. 115 | 116 | .. warning:: ALL DATA WILL BE LOST. Use only for automated testing. 117 | """ 118 | 119 | # Retrieve database name from application config 120 | app = self.db.app 121 | mongo_settings = app.config['MONGODB_SETTINGS'] 122 | database_name = mongo_settings['db'] 123 | 124 | # Flask-MongoEngine is built on MongoEngine, which is built on PyMongo. 125 | # To drop database collections, we need to access the PyMongo Database object, 126 | # which is stored in the PyMongo MongoClient object, 127 | # which is stored in app.extensions['mongoengine'][self]['conn'] 128 | py_mongo_mongo_client = app.extensions['mongoengine'][self.db]['conn'] 129 | py_mongo_database = py_mongo_mongo_client[database_name] 130 | 131 | # Use the PyMongo Database object 132 | for collection_name in py_mongo_database.collection_names(): 133 | py_mongo_database.drop_collection(collection_name) 134 | -------------------------------------------------------------------------------- /flask_user/db_adapters/pynamo_db_adapter.py: -------------------------------------------------------------------------------- 1 | """ This module implements the DBAdapter interface for Pynamo (which uses DynamoDB)""" 2 | 3 | from __future__ import print_function 4 | 5 | # Non-system imports are moved into the methods to make them an optional requirement 6 | 7 | from flask_user.db_adapters import DbAdapterInterface 8 | 9 | 10 | class PynamoDbAdapter(DbAdapterInterface): 11 | """ This object is used to shield Flask-User from PynamoDB specific functions. 12 | """ 13 | 14 | def __init__(self, app, db=None): 15 | """Args: 16 | app(Flask): The Flask appliation instance. 17 | db(PynamoDB): The PynamoDB connection instance. 18 | 19 | | Example: 20 | | app = Flask(__name__) 21 | | db = ignored 22 | | db_adapter = PynamoDbAdapter(app, db) 23 | """ 24 | # This no-op method is defined to show it in Sphinx docs in order 'bysource' 25 | super(PynamoDbAdapter, self).__init__(app, db) 26 | 27 | def add_object(self, object): 28 | """ Add a new object to the database. 29 | 30 | | Session-based ODMs would call something like ``db.session.add(object)``. 31 | | Object-based ODMs would call something like ``object.save()``. 32 | """ 33 | object.save() 34 | 35 | def commit(self): 36 | """Save all modified session objects to the database. 37 | 38 | | Session-based ODMs would call something like ``db.session.commit()``. 39 | | Object-based ODMs would do nothing. 40 | """ 41 | pass 42 | 43 | def delete_object(self, object): 44 | """ Delete object from database. 45 | """ 46 | object.delete() 47 | 48 | def find_objects(self, ObjectClass, **kwargs): 49 | """ Retrieve all objects of type ``ObjectClass``, 50 | matching the specified filters in ``**kwargs`` -- case sensitive. 51 | """ 52 | filter = None 53 | for k, v in kwargs.items(): 54 | cond = ObjectClass.getattr(k) == v 55 | filter = cond if filter is None else filter & cond 56 | 57 | return ObjectClass.scan(filter) 58 | 59 | def find_first_object(self, ObjectClass, **kwargs): 60 | """ Retrieve the first object of type ``ObjectClass``, 61 | matching the specified filters in ``**kwargs`` -- case sensitive. 62 | """ 63 | filter = None 64 | for k, v in kwargs.items(): 65 | cond = getattr(ObjectClass, k) == v 66 | filter = cond if filter is None else filter & cond 67 | 68 | return list(ObjectClass.scan(filter, limit=1))[0] 69 | 70 | def ifind_first_object(self, ObjectClass, **kwargs): 71 | """ Retrieve the first object of type ``ObjectClass``, 72 | matching the specified filters in ``**kwargs`` -- case insensitive. 73 | 74 | | If USER_IFIND_MODE is 'nocase_collation' this method maps to find_first_object(). 75 | | If USER_IFIND_MODE is 'ifind' this method performs a case insensitive find. 76 | """ 77 | from pynamodb.attributes import UnicodeAttribute 78 | 79 | if self.user_manager.USER_IFIND_MODE == 'nocase_collation': 80 | return self.find_first_object(ObjectClass, **kwargs) 81 | 82 | # The below is horrendously slow on a large user database, but DynamoDB 83 | # has no support for case insensitive search, so we have to scan. 84 | # We try and be a little smart and use any non-unicode filters in the scan, thought. 85 | 86 | tfilters = {k: v.lower() for k, v in kwargs.items() if type(getattr(ObjectClass, k)) == UnicodeAttribute} 87 | 88 | ntfilter = None 89 | for k in [k for k in kwargs if k not in tfilters]: 90 | cond = getattr(ObjectClass, k) == kwargs[k] 91 | ntfilter = cond if ntfilter is None else ntfilter & cond 92 | 93 | for o in ObjectClass.scan(ntfilter): 94 | for k in tfilters: 95 | if getattr(o, k, None).lower() != kwargs[k]: 96 | break 97 | else: 98 | # all match 99 | return o 100 | 101 | return None 102 | 103 | def get_object(self, ObjectClass, id): 104 | """ Retrieve object of type ``ObjectClass`` by ``id``. 105 | 106 | | Returns object on success. 107 | | Returns None otherwise. 108 | """ 109 | try: 110 | return ObjectClass.get(id) 111 | except ObjectClass.DoesNotExist: 112 | return None 113 | 114 | def save_object(self, object): 115 | """ Save object to database. 116 | 117 | | Session-based ODMs would do nothing. 118 | | Object-based ODMs would do something like object.save(). 119 | """ 120 | object.save() 121 | 122 | # Database management methods 123 | # --------------------------- 124 | 125 | def __get_classes(self): 126 | db_attrs = ['UserClass', 'UserEmailClass', 'UserInvitationClass', 'RoleClass'] 127 | klasses = [] 128 | for a in db_attrs: 129 | klass = getattr(self.user_manager.db_manager, a, None) 130 | if klass is not None: 131 | klasses.append(klass) 132 | return klasses 133 | 134 | def create_all_tables(self): 135 | """Create database tables for all known database data-models.""" 136 | for klass in self.__get_classes(): 137 | if not klass.exists(): 138 | klass.create_table(read_capacity_units=1, write_capacity_units=1, wait=True) 139 | 140 | def drop_all_tables(self): 141 | """Drop all tables. 142 | 143 | .. warning:: ALL DATA WILL BE LOST. Use only for automated testing. 144 | """ 145 | for klass in self.__get_classes(): 146 | if klass.exists(): 147 | klass.delete_table() 148 | -------------------------------------------------------------------------------- /flask_user/email_adapters/__init__.py: -------------------------------------------------------------------------------- 1 | from .email_adapter_interface import EmailAdapterInterface 2 | from .smtp_email_adapter import SMTPEmailAdapter 3 | from .sendmail_email_adapter import SendmailEmailAdapter 4 | from .sendgrid_email_adapter import SendgridEmailAdapter -------------------------------------------------------------------------------- /flask_user/email_adapters/email_adapter_interface.py: -------------------------------------------------------------------------------- 1 | """This module defines the EmailAdapter interface. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | 9 | from flask_user import ConfigError 10 | 11 | 12 | class EmailAdapterInterface(object): 13 | """ Define the EmailAdapter interface to send emails through various email services.""" 14 | 15 | def __init__(self, app): 16 | """ 17 | Args: 18 | app(Flask): The Flask application instance. 19 | """ 20 | pass 21 | 22 | def send_email_message(self, recipient, subject, html_message, text_message, sender_email, sender_name): 23 | """ Send email message via an email mailer. 24 | 25 | Args: 26 | recipient: Email address or tuple of (Name, Email-address). 27 | subject: Subject line. 28 | html_message: The message body in HTML. 29 | text_message: The message body in plain text. 30 | """ 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /flask_user/email_adapters/sendgrid_email_adapter.py: -------------------------------------------------------------------------------- 1 | """This module implements the EmailAdapter interface for SendGrid. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | 9 | # Non-system imports are moved into the methods to make them an optional requirement 10 | 11 | from flask_user import current_app, ConfigError 12 | from flask_user.email_adapters import EmailAdapterInterface 13 | 14 | SENDGRID_IMPORT_ERROR_MESSAGE = 'The sendgrid package is missing. Install sendgrid with "pip install sendgrid".' 15 | 16 | class SendgridEmailAdapter(EmailAdapterInterface): 17 | """ Implements the EmailAdapter interface to send emails with SendGrid Web API v3 using sendgrid-python.""" 18 | def __init__(self, app): 19 | """Check config settings and setup SendGrid Web API v3. 20 | 21 | Args: 22 | app(Flask): The Flask application instance. 23 | """ 24 | 25 | super(SendgridEmailAdapter, self).__init__(app) 26 | 27 | sendgrid_api_key = app.config.get('SENDGRID_API_KEY') 28 | if not sendgrid_api_key: 29 | raise ConfigError( 30 | "The SENDGRID_API_KEY setting is missing. Set SENDGRID_API_KEY in your app config.") 31 | 32 | # Setup sendgrid-python 33 | try: 34 | from sendgrid import SendGridAPIClient 35 | self.sg = SendGridAPIClient(apikey=sendgrid_api_key) 36 | except ImportError: 37 | raise ConfigError(SENDGRID_IMPORT_ERROR_MESSAGE) 38 | 39 | 40 | def send_email_message(self, recipient, subject, html_message, text_message, sender_email, sender_name): 41 | """ Send email message via sendgrid-python. 42 | 43 | Args: 44 | recipient: Email address or tuple of (Name, Email-address). 45 | subject: Subject line. 46 | html_message: The message body in HTML. 47 | text_message: The message body in plain text. 48 | """ 49 | 50 | if not current_app.testing: # pragma: no cover 51 | try: 52 | # Prepare Sendgrid helper objects 53 | from sendgrid.helpers.mail import Email, Content, Substitution, Mail 54 | from_email = Email(sender_email, sender_name) 55 | to_email = Email(recipient) 56 | text_content = Content('text/plain', text_message) 57 | html_content = Content('text/html', html_message) 58 | # Prepare Sendgrid Mail object 59 | # Note: RFC 1341: text must be first, followed by html 60 | mail = Mail(from_email, subject, to_email, text_content) 61 | mail.add_content(html_content) 62 | # Send mail via the Sendgrid API 63 | response = self.sg.client.mail.send.post(request_body=mail.get()) 64 | print(response.status_code) 65 | print(response.body) 66 | print(response.headers) 67 | except ImportError: 68 | raise ConfigError(SENDGRID_IMPORT_ERROR_MESSAGE) 69 | except Exception as e: 70 | print(e) 71 | print(e.body) 72 | raise 73 | 74 | -------------------------------------------------------------------------------- /flask_user/email_adapters/sendmail_email_adapter.py: -------------------------------------------------------------------------------- 1 | """This module implements the EmailAdapter interface for sendmail. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | 9 | # Non-system imports are moved into the methods to make them an optional requirement 10 | 11 | from flask_user import current_app, ConfigError 12 | from flask_user.email_adapters import EmailAdapterInterface 13 | 14 | 15 | class SendmailEmailAdapter(EmailAdapterInterface): 16 | """ Implements the EmailAdapter interface to send emails with sendmail using Flask-Sendmail.""" 17 | def __init__(self, app, sender_email=None, sender_name=None): 18 | """Check config settings and setup Flask-Sendemail. 19 | 20 | Args: 21 | app(Flask): The Flask application instance. 22 | """ 23 | 24 | super(SendmailEmailAdapter, self).__init__(app) 25 | 26 | # Setup Flask-Mail 27 | try: 28 | from flask_sendmail import Mail 29 | except ImportError: 30 | raise ConfigError( 31 | "The Flask-Sendmail package is missing. Install Flask-Sendmail with 'pip install Flask-Sendmail'.") 32 | self.mail = Mail(app) 33 | 34 | def send_email_message(self, recipient, subject, html_message, text_message, sender_email, sender_name): 35 | """ Send email message via Flask-Sendmail. 36 | 37 | Args: 38 | recipient: Email address or tuple of (Name, Email-address). 39 | subject: Subject line. 40 | html_message: The message body in HTML. 41 | text_message: The message body in plain text. 42 | """ 43 | 44 | if not current_app.testing: # pragma: no cover 45 | 46 | # Prepare email message 47 | from flask_sendmail import Message 48 | message = Message( 49 | subject, 50 | recipients=[recipient], 51 | html=html_message, 52 | body=text_message) 53 | 54 | # Send email message 55 | self.mail.send(message) 56 | 57 | 58 | -------------------------------------------------------------------------------- /flask_user/email_adapters/smtp_email_adapter.py: -------------------------------------------------------------------------------- 1 | """This module implements the EmailAdapter interface for SMTP. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | from __future__ import print_function 8 | import smtplib 9 | import socket 10 | 11 | from flask import current_app 12 | 13 | # Non-system imports are moved into the methods to make them an optional requirement 14 | 15 | from flask_user import ConfigError, EmailError 16 | from flask_user.email_adapters import EmailAdapterInterface 17 | 18 | 19 | class SMTPEmailAdapter(EmailAdapterInterface): 20 | """ Implements the EmailAdapter interface to send emails with SMTP using Flask-Mail.""" 21 | def __init__(self, app): 22 | """Check config settings and setup Flask-Mail. 23 | 24 | Args: 25 | app(Flask): The Flask application instance. 26 | """ 27 | 28 | super(SMTPEmailAdapter, self).__init__(app) 29 | 30 | # Setup Flask-Mail 31 | try: 32 | from flask_mail import Mail 33 | except ImportError: 34 | raise ConfigError( 35 | "The Flask-Mail package is missing. Install Flask-Mail with 'pip install Flask-Mail'.") 36 | self.mail = Mail(app) 37 | 38 | def send_email_message(self, recipient, subject, html_message, text_message, sender_email, sender_name): 39 | """ Send email message via Flask-Mail. 40 | 41 | Args: 42 | recipient: Email address or tuple of (Name, Email-address). 43 | subject: Subject line. 44 | html_message: The message body in HTML. 45 | text_message: The message body in plain text. 46 | """ 47 | 48 | # Construct sender from sender_name and sender_email 49 | sender = '"%s" <%s>' % (sender_name, sender_email) if sender_name else sender_email 50 | 51 | # Send email via SMTP except when we're testing 52 | if not current_app.testing: # pragma: no cover 53 | try: 54 | # Prepare email message 55 | from flask_mail import Message 56 | message = Message( 57 | subject, 58 | sender=sender, 59 | recipients=[recipient], 60 | html=html_message, 61 | body=text_message) 62 | 63 | # Send email message 64 | self.mail.send(message) 65 | 66 | # Print helpful error messages on exceptions 67 | except (socket.gaierror, socket.error) as e: 68 | raise EmailError('SMTP Connection error: Check your MAIL_SERVER and MAIL_PORT settings.') 69 | except smtplib.SMTPAuthenticationError: 70 | raise EmailError('SMTP Authentication error: Check your MAIL_USERNAME and MAIL_PASSWORD settings.') 71 | 72 | -------------------------------------------------------------------------------- /flask_user/legacy_error.py: -------------------------------------------------------------------------------- 1 | """This module implements mock Flask-User v0.6 classes 2 | to warn the developer that they are using v0.6 API calls 3 | against an incompatible v1.0+ Flask-User install. 4 | """ 5 | 6 | # Author: Ling Thio 7 | # Copyright (c) 2013 Ling Thio 8 | 9 | LEGACY_ERROR =\ 10 | """ 11 | Flask-User Legacy ERROR: 12 | ----------------------------------- 13 | You are trying to use the Flask-User v0.6 API 14 | against an _incompatible_ Flask-User v1.0 install. 15 | 16 | Flask-User v1.0: 17 | - Is in its _Alpha_ stage, and not ready for production, 18 | - Is no longer compatible with v0.6, 19 | - Has no changes in the way it customizes form and email templates. 20 | - Has a few changes in its configuration settings, 21 | - Has completely changed the way you customize form classes and views, 22 | - Has completely changed the way you customize passwords and tokens. 23 | 24 | 1) Please downgrade Flask-User back to the latest v0.6 version, or 25 | 2) read https://flask-user.readthedocs.io/en/latest/porting.html 26 | 27 | To downgrade Flask-User: 28 | - Install the latest v0.6 Flask-User 29 | pip install "Flask-User<0.7" 30 | - Make note of the latest Flask-v0.6 version (Flask-User==0.6.{X}) 31 | pip freeze | grep Flask-User 32 | - Update your requirements.txt file to pin the Flask-User version 33 | Flask-User==0.6.{X} 34 | """ 35 | 36 | class DbAdapter(object): 37 | """This is mock Flask-User v0.6 class 38 | to warn the developer that they are using v0.6 API calls 39 | against an incompatible v1.0+ Flask-User install. 40 | """ 41 | 42 | def __init__(self, db, UserClass, UserAuthClass=None, UserEmailClass=None, UserProfileClass=None, UserInvitationClass=None): 43 | raise Exception(LEGACY_ERROR) 44 | 45 | class SQLAlchemyAdapter(DbAdapter): 46 | """This is mock Flask-User v0.6 class 47 | to warn the developer that they are using v0.6 API calls 48 | against an incompatible v1.0+ Flask-User install. 49 | """ 50 | def __init__(self, db, UserClass, UserProfileClass=None, UserAuthClass=None, UserEmailClass=None, UserInvitationClass=None): 51 | raise Exception(LEGACY_ERROR) 52 | 53 | -------------------------------------------------------------------------------- /flask_user/password_manager.py: -------------------------------------------------------------------------------- 1 | """This module implements the PasswordManager for Flask-User. 2 | It uses passlib to hash and verify passwords. 3 | """ 4 | 5 | # Author: Ling Thio 6 | # Copyright (c) 2013 Ling Thio 7 | 8 | from __future__ import print_function 9 | 10 | from flask import current_app 11 | from passlib.context import CryptContext 12 | 13 | 14 | class PasswordManager(object): 15 | """Hash and verify user passwords using passlib """ 16 | 17 | def __init__(self, app): 18 | """ 19 | Create a passlib CryptContext. 20 | 21 | Args: 22 | password_hash(str): The name of a valid passlib password hash. 23 | Examples: ``'bcrypt', 'pbkdf2_sha512', 'sha512_crypt' or 'argon2'``. 24 | 25 | Example: 26 | ``password_manager = PasswordManager('bcrypt')`` 27 | """ 28 | 29 | self.app = app 30 | self.user_manager = app.user_manager 31 | 32 | # Create a passlib CryptContext 33 | self.password_crypt_context = CryptContext( 34 | schemes=self.user_manager.USER_PASSLIB_CRYPTCONTEXT_SCHEMES, 35 | **self.user_manager.USER_PASSLIB_CRYPTCONTEXT_KEYWORDS) 36 | 37 | 38 | def hash_password(self, password): 39 | """Hash plaintext ``password`` using the ``password_hash`` specified in the constructor. 40 | 41 | Args: 42 | password(str): Plaintext password that the user types in. 43 | Returns: 44 | hashed password. 45 | Example: 46 | ``user.password = hash_password('mypassword')`` 47 | """ 48 | 49 | # Use passlib's CryptContext to hash a password 50 | password_hash = self.password_crypt_context.hash(password) 51 | 52 | return password_hash 53 | 54 | 55 | def verify_password(self, password, password_hash): 56 | """Verify plaintext ``password`` against ``hashed password``. 57 | 58 | Args: 59 | password(str): Plaintext password that the user types in. 60 | password_hash(str): Password hash generated by a previous call to ``hash_password()``. 61 | Returns: 62 | | True when ``password`` matches ``password_hash``. 63 | | False otherwise. 64 | Example: 65 | 66 | :: 67 | 68 | if verify_password('mypassword', user.password): 69 | login_user(user) 70 | """ 71 | 72 | # Print deprecation warning if called with (password, user) instead of (password, user.password) 73 | if isinstance(password_hash, self.user_manager.db_manager.UserClass): 74 | print( 75 | 'Deprecation warning: verify_password(password, user) has been changed'\ 76 | ' to: verify_password(password, password_hash). The user param will be deprecated.'\ 77 | ' Please change your call with verify_password(password, user) into'\ 78 | ' a call with verify_password(password, user.password)' 79 | ' as soon as possible.') 80 | password_hash = password_hash.password # effectively user.password 81 | 82 | # Use passlib's CryptContext to verify a password 83 | return self.password_crypt_context.verify(password, password_hash) 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /flask_user/signals.py: -------------------------------------------------------------------------------- 1 | """ This file creates event notification signals for Flask-User. 2 | Signals are based on Flask.signals which are based on the blinker signals. 3 | """ 4 | 5 | # Copyright (c) 2013 by Ling Thio 6 | # Author: Ling Thio (ling.thio@gmail.com) 7 | # License: Simplified BSD License, see LICENSE.txt for more details. 8 | 9 | 10 | from flask.signals import Namespace 11 | 12 | _signals = Namespace() # Place Flask-User signals in our own namespace 13 | 14 | # ******************* 15 | # ** Flask Signals ** 16 | # ******************* 17 | # Flask signals are based on blinker. Neither Flask nor Flask-User installs blinker 18 | # If you plan to use signals, please install blinker with 'pip install blinker' 19 | # See http://flask.pocoo.org/docs/signals/ 20 | 21 | # Sent when a user changed their password 22 | user_changed_password = _signals.signal('user.user_changed_password') 23 | 24 | # Sent when a user changed their username 25 | user_changed_username = _signals.signal('user.user_changed_username') 26 | 27 | # Sent when a user confirmed their email 28 | user_confirmed_email = _signals.signal('user.user_confirmed_email') 29 | 30 | # Sent when a user submitted a password reset request 31 | user_forgot_password = _signals.signal('user.forgot_password') 32 | 33 | # Sent when a user logged in 34 | user_logged_in = _signals.signal('user.user_logged_in') 35 | 36 | # Sent when a user logged out 37 | user_logged_out = _signals.signal('user.user_logged_out') 38 | 39 | # Sent when a user registered a new account 40 | user_registered = _signals.signal('user.user_registered') 41 | 42 | # Signal sent just after a password was reset 43 | user_reset_password = _signals.signal('user.user_reset_password') 44 | 45 | # Signal sent just after a user sent an invitation # TODO: Not yet implemented 46 | user_sent_invitation = _signals.signal('user.user_sent_invitation') 47 | 48 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/_authorized_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_common_base.html' %} 2 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/_common_base.html: -------------------------------------------------------------------------------- 1 | {% extends "flask_user_layout.html" %} 2 | 3 | {% block main %} 4 |
5 |
6 |
7 | {% block content %}{% endblock %} 8 |
9 |
10 |
11 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%} 2 |
3 | {% if field.type != 'HiddenField' and label_visible %} 4 | {% if not label %}{% set label=field.label.text %}{% endif %} 5 | 6 | {% endif %} 7 | {{ field(class_='form-control', **kwargs) }} 8 | {% if field.errors %} 9 | {% for e in field.errors %} 10 |

{{ e }}

11 | {% endfor %} 12 | {% endif %} 13 |
14 | {%- endmacro %} 15 | 16 | {% macro render_checkbox_field(field, label=None) -%} 17 | {% if not label %}{% set label=field.label.text %}{% endif %} 18 |
19 | 22 |
23 | {%- endmacro %} 24 | 25 | {% macro render_radio_field(field) -%} 26 | {% for value, label, checked in field.iter_choices() %} 27 |
28 | 32 |
33 | {% endfor %} 34 | {%- endmacro %} 35 | 36 | {% macro render_submit_field(field, label=None, tabindex=None) -%} 37 | {% if not label %}{% set label=field.label.text %}{% endif %} 38 | {##} 39 | 42 | {%- endmacro %} 43 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/_public_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_common_base.html' %} 2 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_authorized_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Change password{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.old_password, tabindex=10) }} 10 | {{ render_field(form.new_password, tabindex=20) }} 11 | {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} 12 | {{ render_field(form.retype_password, tabindex=30) }} 13 | {% endif %} 14 | {{ render_submit_field(form.submit, tabindex=90) }} 15 |
16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/change_username.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_authorized_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Change username{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.new_username, tabindex=10) }} 10 | {{ render_field(form.old_password, tabindex=20) }} 11 | {{ render_submit_field(form.submit, tabindex=90) }} 12 |
13 | 14 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/edit_user_profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_authorized_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} 5 |

{%trans%}User profile{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {% for field in form %} 10 | {% if not field.flags.hidden %} 11 | {% if field.type=='SubmitField' %} 12 | {{ render_submit_field(field, tabindex=loop.index*10) }} 13 | {% else %} 14 | {{ render_field(field, tabindex=loop.index*10) }} 15 | {% endif %} 16 | {% endif %} 17 | {% endfor %} 18 |
19 |
20 | 21 | {% if not user_manager.USER_ENABLE_AUTH0 %} 22 | {% if user_manager.USER_ENABLE_CHANGE_USERNAME %} 23 |

{%trans%}Change username{%endtrans%}

24 | {% endif %} 25 | {% if user_manager.USER_ENABLE_CHANGE_PASSWORD %} 26 |

{%trans%}Change password{%endtrans%}

27 | {% endif %} 28 | {% endif %} 29 | 30 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/base_message.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.email }},

2 | 3 | {% block message %} 4 | {% endblock %} 5 | 6 |

Sincerely,
7 | {{ app_name }} 8 |

-------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/base_message.txt: -------------------------------------------------------------------------------- 1 | Dear User, 2 | 3 | {% block message %} 4 | {% endblock %} 5 | 6 | Sincerely, 7 | {{ app_name }} 8 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/base_subject.txt: -------------------------------------------------------------------------------- 1 | {{ app_name }} - {% block subject %}{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/confirm_email_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 |

You will need to confirm your email to start using {{ app_name }}.

5 | 6 |

If you initiated this confirmation, please click on the link below:
7 |     Confirm your email.

8 | 9 |

If you did not initiate this confirmation, you may safely ignore this email.

10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/confirm_email_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | You will need to confirm your email to start using {{ app_name }}. 5 | 6 | If you initiated this registration, please visit the link below: 7 | {{ confirm_email_link }} 8 | 9 | If you did not initiate this registration, you may safely ignore this email. 10 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/confirm_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}Email Confirmation{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/invite_user_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 | 5 |

You have been invited to join {{ app_name }}!

6 | 7 |

To register an account, please click on the link below:
8 |     Join {{ app_name }}.

9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/invite_user_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | You have been invited to join {{ app_name }}. 5 | 6 | To register an account, please click on the link below: 7 | {{ accept_invitation_link }} 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/invite_user_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}Invitation{% endblock %} 4 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/password_changed_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 |

Your password has been changed.

5 | {% if user_manager.USER_ENABLE_FORGOT_PASSWORD %} 6 |

If you did not initiate this password change, click here to reset it.

7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/password_changed_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | Your password has been changed. 5 | 6 | {% if user_manager.USER_ENABLE_FORGOT_PASSWORD -%} 7 | If you did not initiate this password change, click the link below to reset it. 8 | {{ url_for('user.forgot_password', _external=True) }} 9 | {% endif -%} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/password_changed_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}Your password has been changed{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/registered_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 | 5 |

Thank you for registering with {{ app_name }}.

6 | 7 | {% if confirm_email_link -%} 8 |

You will need to confirm your email next.

9 | 10 |

If you initiated this registration, please click on the link below:
11 |     Confirm your email.

12 | 13 |

If you did not initiate this registration, you may safely ignore this email.

14 | {%- endif %} 15 | 16 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/registered_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | Thank you for registering with {{ app_name }}. 5 | 6 | {% if confirm_email_link -%} 7 | You will need to confirm your email next. 8 | 9 | If you initiated this registration, please visit the link below: 10 | {{ confirm_email_link }} 11 | 12 | If you did not initiate this registration, you may safely ignore this email. 13 | 14 | {%- endif %} 15 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/registered_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}{% if user_manager.enable_confirm_email and not user.confirmed_at %}Confirm your email{% else %}Thank you for registering{% endif %}{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/reset_password_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 | 5 |

We have received your password reset request.

6 | 7 |

If you initiated this request, please click on the link below:
8 |     Reset password.

9 | 10 |

If you did not initiate this password reset, you may safely ignore this email.

11 | 12 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/reset_password_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | We have received your password reset request. 5 | 6 | If you initiated this request, please click on the link below: 7 | {{ reset_password_link }} 8 | 9 | If you did not initiate this password reset, you may safely ignore this email. 10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/reset_password_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}Reset password{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/username_changed_message.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.html' %} 2 | 3 | {% block message %} 4 |

Your username has been changed.

5 |

If you did not initiate this username change, please sign in (using your email address) and change your password.

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/username_changed_message.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_message.txt' %} 2 | 3 | {% block message %} 4 | Your username has been changed. 5 | 6 | If you did not initiate this username change, please sign in (using your email address) and change your password. 7 | {{ url_for('user.login', _external=True) }} 8 | {% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/emails/username_changed_subject.txt: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/emails/base_subject.txt' %} 2 | 3 | {% block subject %}Your username has been changed{% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Forgot Password{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.email, tabindex=10) }} 10 | {{ render_submit_field(form.submit, tabindex=90) }} 11 |
12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/invite_user.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_authorized_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Invite User{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.email, tabindex=10) }} 10 | {{ render_submit_field(form.submit, tabindex=90) }} 11 |
12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} 5 |

{%trans%}Sign in{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | 10 | {# Username or Email field #} 11 | {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} 12 |
13 | {# Label on left, "New here? Register." on right #} 14 |
15 |
16 | 17 |
18 |
19 | {% if user_manager.USER_ENABLE_REGISTER and not user_manager.USER_REQUIRE_INVITATION %} 20 | 21 | {%trans%}New here? Register.{%endtrans%} 22 | {% endif %} 23 |
24 |
25 | {{ field(class_='form-control', tabindex=110) }} 26 | {% if field.errors %} 27 | {% for e in field.errors %} 28 |

{{ e }}

29 | {% endfor %} 30 | {% endif %} 31 |
32 | 33 | {# Password field #} 34 | {% set field = form.password %} 35 |
36 | {# Label on left, "Forgot your Password?" on right #} 37 |
38 |
39 | 40 |
41 |
42 | {% if user_manager.USER_ENABLE_FORGOT_PASSWORD %} 43 | 44 | {%trans%}Forgot your Password?{%endtrans%} 45 | {% endif %} 46 |
47 |
48 | {{ field(class_='form-control', tabindex=120) }} 49 | {% if field.errors %} 50 | {% for e in field.errors %} 51 |

{{ e }}

52 | {% endfor %} 53 | {% endif %} 54 |
55 | 56 | {# Remember me #} 57 | {% if user_manager.USER_ENABLE_REMEMBER_ME %} 58 | {{ render_checkbox_field(login_form.remember_me, tabindex=130) }} 59 | {% endif %} 60 | 61 | {# Submit button #} 62 | {{ render_submit_field(form.submit, tabindex=180) }} 63 |
64 | 65 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/login_auth0.html: -------------------------------------------------------------------------------- 1 | {% extends "flask_user_layout.html" %} 2 | 3 | {% block extra_css %} 4 | 21 | {% endblock %} 22 | 23 | {% block content %} 24 |
25 | Sign in 27 |
28 | {% endblock %} 29 | 30 | {% block extra_js %} 31 | {# Load Auth0 javascript library #} 32 | 33 | 34 | 70 | {% endblock %} 71 | 72 | -------------------------------------------------------------------------------- /flask_user/templates/flask_user/login_or_register.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} 5 | 6 |
7 |
8 | 9 |

{%trans%}Sign in{%endtrans%}

10 | 11 | {# ** Login form ** #} 12 |
13 | {{ login_form.hidden_tag() }} 14 | 15 | {# Username or Email #} 16 | {% set field = login_form.username if user_manager.USER_ENABLE_USERNAME else login_form.email %} 17 | {{ render_field(field, tabindex=110) }} 18 | 19 | {# Password #} 20 | {{ render_field(login_form.password, tabindex=120) }} 21 | 22 | {# Remember me #} 23 | {% if user_manager.USER_ENABLE_REMEMBER_ME %} 24 | {{ render_checkbox_field(login_form.remember_me, tabindex=130) }} 25 | {% endif %} 26 | 27 | {# Submit button #} 28 | {{ render_submit_field(login_form.submit, tabindex=180) }} 29 |
30 | {% if user_manager.USER_ENABLE_FORGOT_PASSWORD %} 31 |

32 |
33 | 34 | {%trans%}Forgot your Password?{%endtrans%} 35 |

36 | {% endif %} 37 | 38 |
39 |
40 | 41 |

{%trans%}Register{%endtrans%}

42 | 43 | {# ** Register form ** #} 44 |
45 | {{ register_form.hidden_tag() }} 46 | 47 | {# Username or Email #} 48 | {% set field = register_form.username if user_manager.USER_ENABLE_USERNAME else register_form.email %} 49 | {{ render_field(field, tabindex=210) }} 50 | 51 | {% if user_manager.USER_ENABLE_EMAIL and user_manager.USER_ENABLE_USERNAME %} 52 | {{ render_field(register_form.email, tabindex=220) }} 53 | {% endif %} 54 | 55 | {{ render_field(register_form.password, tabindex=230) }} 56 | 57 | {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} 58 | {{ render_field(register_form.retype_password, tabindex=240) }} 59 | {% endif %} 60 | 61 | {{ render_submit_field(register_form.submit, tabindex=280) }} 62 |
63 | 64 |
65 |
66 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/manage_emails.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_authorized_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Manage Emails{%endtrans%}

6 | 7 | 8 | 9 | {% for user_email in user_emails %} 10 | 11 | 12 | 19 | 29 | 30 | {% endfor %} 31 |
EmailStatusActions
{{ user_email.email }} 13 | {% if user_email.email_confirmed_at %} 14 | Confirmed 15 | {% else %} 16 | Confirm Email 17 | {% endif %} 18 | 20 | {% if user_email.is_primary %} 21 | Primary email 22 | {% else %} 23 | {% if user_email.email_confirmed_at %} 24 | Make primary | 25 | {% endif %} 26 | Delete 27 | {% endif %} 28 |
32 | 33 |
34 | {{ form.hidden_tag() }} 35 | {{ render_field(form.email) }} 36 | {{ render_submit_field(form.submit) }} 37 |
38 | 39 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Register{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | 10 | {# Username or Email #} 11 | {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} 12 |
13 | {# Label on left, "Already registered? Sign in." on right #} 14 |
15 |
16 | 17 |
18 |
19 | {% if user_manager.USER_ENABLE_REGISTER %} 20 | 21 | {%trans%}Already registered? Sign in.{%endtrans%} 22 | {% endif %} 23 |
24 |
25 | {{ field(class_='form-control', tabindex=210) }} 26 | {% if field.errors %} 27 | {% for e in field.errors %} 28 |

{{ e }}

29 | {% endfor %} 30 | {% endif %} 31 |
32 | 33 | {% if user_manager.USER_ENABLE_EMAIL and user_manager.USER_ENABLE_USERNAME %} 34 | {{ render_field(form.email, tabindex=220) }} 35 | {% endif %} 36 | 37 | {{ render_field(form.password, tabindex=230) }} 38 | 39 | {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} 40 | {{ render_field(form.retype_password, tabindex=240) }} 41 | {% endif %} 42 | 43 | {{ render_submit_field(form.submit, tabindex=280) }} 44 |
45 | 46 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/resend_confirm_email.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Resend Confirmation Email{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.email, tabindex=10) }} 10 | {{ render_submit_field(form.submit, tabindex=90) }} 11 |
12 | 13 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_user/_public_base.html' %} 2 | 3 | {% block content %} 4 | {% from "flask_user/_macros.html" import render_field, render_submit_field %} 5 |

{%trans%}Reset Password{%endtrans%}

6 | 7 |
8 | {{ form.hidden_tag() }} 9 | {{ render_field(form.new_password, tabindex=10) }} 10 | {% if user_manager.USER_REQUIRE_RETYPE_PASSWORD %} 11 | {{ render_field(form.retype_password, tabindex=20) }} 12 | {% endif %} 13 | {{ render_submit_field(form.submit, tabindex=90) }} 14 |
15 | 16 | {% endblock %} -------------------------------------------------------------------------------- /flask_user/templates/flask_user_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ user_manager.USER_APP_NAME }} 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 26 | 27 | {# *** Allow sub-templates to insert extra html to the head section *** #} 28 | {% block extra_css %}{% endblock %} 29 | 30 | 31 | 32 | 33 | {% block body %} 34 |
35 | 36 |
37 | {% if call_or_get(current_user.is_authenticated) %} 38 | {{ current_user.username or current_user.email }} 39 |   |   40 | {%trans%}Sign out{%endtrans%} 41 | {% else %} 42 | {%trans%}Sign in{%endtrans%} 43 | {% endif %} 44 |
45 |
46 | {% block menu %} 47 | 50 | {% endblock %} 51 |
52 | 53 |
54 | {# One-time system messages called Flash messages #} 55 | {% block flash_messages %} 56 | {%- with messages = get_flashed_messages(with_categories=true) -%} 57 | {% if messages %} 58 | {% for category, message in messages %} 59 | {% if category=='error' %} 60 | {% set category='danger' %} 61 | {% endif %} 62 |
{{ message|safe }}
63 | {% endfor %} 64 | {% endif %} 65 | {%- endwith %} 66 | {% endblock %} 67 | 68 | {% block main %} 69 | {% block content %}{% endblock %} 70 | {% endblock %} 71 |
72 | 73 |
74 |
75 | 79 | {% endblock %} 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {# *** Allow sub-templates to insert extra html to the bottom of the body *** #} 88 | {% block extra_js %}{% endblock %} 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /flask_user/tests/.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | 3 | [run] 4 | omit = 5 | flask_user/db_adapters/dynamo_db_adapter.py 6 | flask_user/tests/* 7 | 8 | 9 | [report] 10 | exclude_lines = 11 | # Have to re-enable the standard pragma 12 | pragma: no cover 13 | 14 | # Don't complain about missing debug-only code: 15 | def __repr__ 16 | if self\.debug 17 | 18 | # Don't complain if tests don't hit defensive assertion code: 19 | except .*\: 20 | raise ConfigError 21 | raise NotImplementedError 22 | raise KeyError 23 | raise TypeError 24 | raise Exception\(LEGACY_ERROR\) 25 | if not ctx: 26 | return self.unauthorized_view 27 | 28 | # Misc 29 | delattr\(self 30 | 31 | # Don't complain if non-runnable code isn't run: 32 | if 0: 33 | if __name__ == .__main__.: 34 | -------------------------------------------------------------------------------- /flask_user/tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'lingthio' 2 | -------------------------------------------------------------------------------- /flask_user/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from flask_user.tests.tst_app import app as the_app, init_app 5 | from flask_user.tests.tst_utils import TstClient 6 | 7 | @pytest.fixture(scope='session') 8 | def app(request): 9 | test_config = dict( 10 | SQLALCHEMY_DATABASE_URI='sqlite:///:memory:', # In-memory sqlite DB 11 | TESTING=True, # Propagate exceptions (don't show 500 error page) 12 | WTF_CSRF_ENABLED=False, # Disable CSRF token in Flask-Wtf 13 | LOGIN_DISABLED=False, # Enable @register_required while app.testing=True 14 | MAIL_SUPPRESS_SEND=True, # Suppress the sending of emails 15 | SERVER_NAME='localhost' # Enable url_for() without request context 16 | ) 17 | 18 | # Create app with test settings 19 | init_app(the_app, test_config) 20 | 21 | # Establish an application context before running the tests. 22 | ctx = the_app.app_context() 23 | ctx.push() 24 | 25 | def teardown(): 26 | ctx.pop() 27 | 28 | request.addfinalizer(teardown) 29 | return the_app 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | def db(app, request): 34 | # Retrieve db_adapter through App 35 | db_manager = app.user_manager.db_manager 36 | 37 | # Create all tables 38 | db_manager.create_all_tables() 39 | 40 | # Define a teardown() function to drop all tables at end of test 41 | def teardown(): 42 | db_manager.drop_all_tables() 43 | 44 | # Register teardown function 45 | request.addfinalizer(teardown) 46 | 47 | return app.db 48 | 49 | 50 | @pytest.fixture(scope='function') 51 | def session(db, request): 52 | """Creates a new database session for a test.""" 53 | connection = db.engine.connect() 54 | transaction = connection.begin() 55 | 56 | options = dict(bind=connection, binds={}) 57 | session = db.create_scoped_session(options=options) 58 | 59 | db.session = session 60 | 61 | def teardown(): 62 | transaction.rollback() 63 | connection.close() 64 | session.remove() 65 | 66 | request.addfinalizer(teardown) 67 | return session 68 | 69 | @pytest.fixture(scope='session') 70 | def client(app, db, request): 71 | return TstClient(app.test_client(), db) 72 | 73 | -------------------------------------------------------------------------------- /flask_user/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | from .utils import utils_prepare_user 2 | 3 | # Make sure that uncovered lines are covered 4 | def test_misc(app): 5 | um = app.user_manager 6 | 7 | user = utils_prepare_user(app) 8 | 9 | # Generate token with data-item other than int or string 10 | um.token_manager.generate_token(1.1) 11 | 12 | # Hash password with old API 13 | um.password_manager.verify_password('password', user) -------------------------------------------------------------------------------- /flask_user/tests/test_roles.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask import current_app 4 | from flask_login import login_user, logout_user 5 | 6 | from flask_user import login_required, roles_accepted, roles_required, allow_unconfirmed_email 7 | 8 | def test_decorators(app, request): 9 | 10 | # Setup view functions with decorators 11 | # ------------------------------------ 12 | 13 | def standard_view(): 14 | return 'view' 15 | 16 | @login_required 17 | def login_required_view(): 18 | return 'view' 19 | 20 | @roles_accepted('A', 'B') 21 | def roles_accepted_view(): 22 | return 'view' 23 | 24 | @roles_required('A', 'B') 25 | def roles_required_view(): 26 | return 'view' 27 | 28 | @allow_unconfirmed_email 29 | def without_view(): 30 | return 'view' 31 | 32 | @allow_unconfirmed_email 33 | @login_required 34 | def login_required_without(): 35 | return 'view' 36 | 37 | @allow_unconfirmed_email 38 | @roles_accepted('A', 'B') 39 | def roles_accepted_without(): 40 | return 'view' 41 | 42 | @allow_unconfirmed_email 43 | @roles_required('A', 'B') 44 | def roles_required_without(): 45 | return 'view' 46 | 47 | um = current_app.user_manager 48 | User = um.db_manager.UserClass 49 | 50 | # Create Mock objects (They will NOT be persisted to DB. We can use dummy IDs here) 51 | user = um.db_manager.UserClass(id=1, password='abcdefgh', email_confirmed_at=None) 52 | role_a = um.db_manager.RoleClass(id=1, name='A') 53 | role_b = um.db_manager.RoleClass(id=2, name='B') 54 | 55 | with current_app.test_request_context(): 56 | # Test decorators with an unauthenticated user 57 | assert standard_view() == 'view' 58 | assert login_required_view() != 'view' 59 | assert roles_accepted_view() != 'view' 60 | assert roles_required_view() != 'view' 61 | assert without_view() != 'view' 62 | assert login_required_without() != 'view' 63 | assert roles_accepted_without() != 'view' 64 | assert roles_required_without() != 'view' 65 | 66 | # Test decorators with a logged in user without a confirmed email 67 | login_user(user) 68 | assert login_required_view() != 'view' 69 | assert roles_accepted_view() != 'view' 70 | assert roles_required_view() != 'view' 71 | assert without_view() == 'view' 72 | assert login_required_without() == 'view' 73 | 74 | user.roles = [] 75 | assert roles_accepted_view() != 'view' 76 | assert roles_required_view() != 'view' 77 | assert roles_accepted_without() != 'view' 78 | assert roles_required_without() != 'view' 79 | 80 | user.roles = [role_a] 81 | assert roles_accepted_view() != 'view' 82 | assert roles_required_view() != 'view' 83 | assert roles_accepted_without() == 'view' 84 | assert roles_required_without() != 'view' 85 | 86 | user.roles = [role_b] 87 | assert roles_accepted_view() != 'view' 88 | assert roles_required_view() != 'view' 89 | assert roles_accepted_without() == 'view' 90 | assert roles_required_without() != 'view' 91 | 92 | user.roles = [role_a, role_b] 93 | assert roles_accepted_view() != 'view' 94 | assert roles_required_view() != 'view' 95 | assert roles_accepted_without() == 'view' 96 | assert roles_required_without() == 'view' 97 | 98 | # Test decorators with a logged in user with a confirmed email 99 | user.email_confirmed_at = datetime.datetime.utcnow() 100 | assert login_required_view() == 'view' 101 | assert without_view() == 'view' 102 | assert login_required_without() == 'view' 103 | 104 | user.roles = [] 105 | assert roles_accepted_view() != 'view' 106 | assert roles_required_view() != 'view' 107 | assert roles_accepted_without() != 'view' 108 | assert roles_required_without() != 'view' 109 | 110 | user.roles = [role_a] 111 | assert roles_accepted_view() == 'view' 112 | assert roles_required_view() != 'view' 113 | assert roles_accepted_without() == 'view' 114 | assert roles_required_without() != 'view' 115 | 116 | user.roles = [role_b] 117 | assert roles_accepted_view() == 'view' 118 | assert roles_required_view() != 'view' 119 | assert roles_accepted_without() == 'view' 120 | assert roles_required_without() != 'view' 121 | 122 | user.roles = [role_a, role_b] 123 | assert roles_accepted_view() == 'view' 124 | assert roles_required_view() == 'view' 125 | assert roles_accepted_without() == 'view' 126 | assert roles_required_without() == 'view' 127 | 128 | logout_user() 129 | 130 | -------------------------------------------------------------------------------- /flask_user/tests/test_zzz_db_adapters.py: -------------------------------------------------------------------------------- 1 | from flask_user.db_adapters import MongoDbAdapter 2 | from flask_user.db_manager import DBManager 3 | 4 | 5 | def test_mongoengine_db_adapter(app): 6 | # Make sure Flask-MongoEngine and MongoEngine are installed 7 | try: 8 | from flask_mongoengine import MongoEngine 9 | from mongoengine import connect 10 | except ImportError: 11 | return 12 | 13 | # Import ConnectionError or MongoEngineConnectionError, depending on version 14 | try: 15 | from mongoengine.connection import MongoEngineConnectionError # 0.11.0+ 16 | except ImportError: 17 | from mongoengine.connection import ConnectionError as MongoEngineConnectionError # 0.10.9 and down 18 | 19 | # Make sure that a MongoDB server is up and running on localhost:27017 20 | try: 21 | db = MongoEngine(app) 22 | except MongoEngineConnectionError: 23 | return 24 | 25 | class User(db.Document): 26 | username = db.StringField(default='') 27 | roles = db.ListField(db.StringField(), default=[]) 28 | 29 | db_manager = DBManager(app, db, UserClass=User) 30 | 31 | username = 'username' 32 | 33 | # Test create_all_tables 34 | db_manager.drop_all_tables() 35 | db_manager.create_all_tables() 36 | 37 | # Test add_object 38 | user1 = db_manager.add_user(username='username') 39 | user2 = db_manager.add_user(username='SecondUser') 40 | db_manager.commit() 41 | 42 | # Test tokenizing MongoDB IDs 43 | token_manager = app.user_manager.token_manager 44 | token = token_manager.generate_token(user1.id) 45 | assert(token_manager.verify_token(token, 3600)) 46 | 47 | # Test get_object 48 | user = db_manager.get_user_by_id('1234567890ab1234567890ab') 49 | assert user==None 50 | user = db_manager.get_user_by_id(user1.id) 51 | assert user==user1 52 | 53 | # Test find methods 54 | user = db_manager.find_user_by_username('Xyz') 55 | assert user==None 56 | user = db_manager.find_user_by_username(username) 57 | assert user==user1 58 | 59 | # Test find_objects, directly through adapter 60 | users = db_manager.db_adapter.find_objects(User) 61 | 62 | # Test save_object 63 | user.username='NewUsername' 64 | db_manager.save_object(user) 65 | db_manager.commit() 66 | user = db_manager.get_user_by_id(user.id) 67 | assert user==user1 68 | assert user.username=='NewUsername' 69 | 70 | # Test user_role methods 71 | db_manager.add_user_role(user1, 'Admin') 72 | db_manager.add_user_role(user1, 'Agent') 73 | user_roles = db_manager.get_user_roles(user1) 74 | assert user_roles == ['Admin', 'Agent'] 75 | 76 | # Test delete_object 77 | user1_id = user1.id 78 | db_manager.delete_object(user1) 79 | db_manager.commit() 80 | user = db_manager.get_user_by_id(user1_id) 81 | assert user==None 82 | user = db_manager.get_user_by_id(user2.id) 83 | assert user==user2 84 | 85 | # Test drop_all_tables 86 | db_manager.drop_all_tables() 87 | user = db_manager.get_user_by_id(user2.id) 88 | assert user==None 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /flask_user/tests/tst_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests.utils 3 | ----------- 4 | Utility class for Flask-User automated tests 5 | 6 | :copyright: (c) 2013 by Ling Thio 7 | :author: Ling Thio (ling.thio@gmail.com) 8 | :license: Simplified BSD License, see LICENSE.txt for more details. 9 | """ 10 | from __future__ import print_function 11 | from flask import url_for 12 | 13 | # Checks to see if response.data contains the specified string. 14 | def response_has_string(response, string): 15 | assert response.status_code == 200 16 | # In Python3, response.data is and string is 17 | # hence the use of 'str.encode(string)' 18 | return response.data.find(str.encode(string)) >= 0 19 | 20 | # Checks to see if response.data contains the string 'has-error'. 21 | def response_has_errors(response): 22 | return response_has_string(response, 'has-error') or response_has_string(response, 'alert-danger') 23 | 24 | # Checks to see if response.data contains no 'has-error' strings 25 | def response_has_no_errors(response): 26 | has_errors = response_has_errors(response) 27 | if has_errors: 28 | print(response.data) 29 | return not has_errors 30 | 31 | class TstClient(object): 32 | """ 33 | Utility class for tests 34 | """ 35 | def __init__(self, client, db): 36 | self.client = client 37 | self.db = db 38 | 39 | def get_valid_page(self, url): 40 | """ 41 | GET url and assert that the response contains no errors. 42 | """ 43 | response = self.client.get(url, follow_redirects=True) 44 | assert response.status_code == 200, "GET %s returned %d" % (url, response.status_code) 45 | assert response_has_no_errors(response), "GET %s returned an error" % url 46 | return response 47 | 48 | def get_invalid_page(self, url, expected_error): 49 | """ 50 | GET url and assert that the response contains an expected error. 51 | """ 52 | response = self.client.get(url, follow_redirects=True) 53 | assert response.status_code == 200, "POST %s returned %d" % (url, response.status_code) 54 | response_has_error = response_has_string(response, expected_error) 55 | if not response_has_error: 56 | print(response.data) 57 | assert response_has_error, "POST %s did not contain '%s' error" % (url, expected_error) 58 | return response 59 | 60 | def post_valid_form(self, url, **kwargs): 61 | """ 62 | POST url and assert that the response contains no errors. 63 | """ 64 | response = self.client.post(url, data=kwargs, follow_redirects=True) 65 | assert response.status_code == 200, "POST %s returned %d" % (url, response.status_code) 66 | assert response_has_no_errors(response), "post_valid_form(%s) returned an error" % url 67 | return response 68 | 69 | def post_invalid_form(self, url, expected_error, **kwargs): 70 | """ 71 | POST url and assert that the response contains an expected error. 72 | """ 73 | response = self.client.post(url, data=kwargs, follow_redirects=True) 74 | assert response.status_code == 200, "POST %s returned %d" % (url, response.status_code) 75 | response_has_error = response_has_string(response, expected_error) 76 | if not response_has_error: 77 | print(response.data) 78 | assert response_has_error, "POST %s did not contain '%s' error" % (url, expected_error) 79 | return response 80 | 81 | def login(self, **kwargs): 82 | """ 83 | Log new user in with username/password or email/password. 84 | """ 85 | url = url_for('user.login') 86 | return self.post_valid_form(url, **kwargs) 87 | 88 | def logout(self, **kwargs): 89 | """ 90 | Log current user out. 91 | """ 92 | url = url_for('user.logout') 93 | response = self.client.get(url, follow_redirects=True) 94 | assert response.status_code == 200 95 | 96 | -------------------------------------------------------------------------------- /flask_user/tests/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask_login import login_user, current_user 4 | 5 | def utils_prepare_user(app): 6 | um = app.user_manager 7 | db_manager=um.db_manager 8 | db_adapter = db_manager.db_adapter 9 | User = db_manager.UserClass 10 | UserEmail = db_manager.UserEmailClass 11 | 12 | # Get or create test user 13 | test_user = User.query.filter(User.email=='testuser@example.com').first() 14 | if not test_user: 15 | password_hash = um.password_manager.hash_password('Password1') 16 | test_user = User(password=password_hash) 17 | db_adapter.add_object(test_user) 18 | 19 | # NB: password_manager.hash_password() seems to mess up the request context 20 | test_user.active = True 21 | test_user.username = 'testuser' 22 | test_user.email = 'testuser@example.com' 23 | test_user.email_confirmed_at = datetime.datetime.utcnow() 24 | test_user.first_name = 'Firstname' 25 | test_user.last_name = 'Lastname' 26 | db_adapter.save_object(test_user) 27 | db_adapter.commit() 28 | 29 | return test_user 30 | -------------------------------------------------------------------------------- /flask_user/translation_utils.py: -------------------------------------------------------------------------------- 1 | """ This module implements utility functions to offer translations. 2 | It uses Flask-BabelEx to manage domain specific translation files. 3 | """ 4 | 5 | # Author: Ling Thio 6 | # Copyright (c) 2013 Ling Thio 7 | 8 | import os 9 | from flask import request 10 | 11 | _translations_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'translations') 12 | 13 | # Load Flask-User translations, if Flask-BabelEx has been installed 14 | try: 15 | from flask_babelex import Domain 16 | 17 | # Retrieve Flask-User translations from the flask_user/translations directory 18 | domain_translations = Domain(_translations_dir, domain='flask_user') 19 | except ImportError: 20 | domain_translations = None 21 | 22 | def gettext(string, **variables): 23 | return domain_translations.gettext(string, **variables) if domain_translations else string % variables 24 | 25 | def lazy_gettext(string, **variables): 26 | return domain_translations.lazy_gettext(string, **variables) if domain_translations else string % variables 27 | 28 | def get_language_codes(): 29 | language_codes = [] 30 | for folder in os.listdir(_translations_dir): 31 | locale_dir = os.path.join(_translations_dir, folder, 'LC_MESSAGES') 32 | if not os.path.isdir(locale_dir): 33 | continue 34 | language_codes.append(folder) 35 | return language_codes 36 | 37 | def init_translations(babel): 38 | if babel: 39 | babel._default_domain = domain_translations 40 | 41 | # Install a language selector if one has not yet been installed 42 | if babel.locale_selector_func is None: 43 | # Define a language selector 44 | def get_locale(): 45 | # Retrieve a list of available language codes 46 | available_language_codes = get_language_codes() 47 | # Match list with languages from the user's browser's accept header 48 | language_code = request.accept_languages.best_match(available_language_codes) 49 | return language_code 50 | 51 | # Install the language selector 52 | babel.locale_selector_func = get_locale 53 | -------------------------------------------------------------------------------- /flask_user/translations/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /flask_user/user_manager__utils.py: -------------------------------------------------------------------------------- 1 | """This module implements UserManager utility methods. 2 | """ 3 | 4 | # Author: Ling Thio 5 | # Copyright (c) 2013 Ling Thio 6 | 7 | try: 8 | from urllib.parse import urlsplit, urlunsplit # Python 3 9 | except ImportError: 10 | from urlparse import urlsplit, urlunsplit # Python 2 11 | 12 | 13 | from flask_login import current_user 14 | 15 | 16 | # This class mixes into the UserManager class. 17 | # Mixins allow for maintaining code and docs across several files. 18 | class UserManager__Utils(object): 19 | """Flask-User utility methods.""" 20 | 21 | # Flask-Login 0.2 uses functions while 0.3 uses properties 22 | def call_or_get(self, function_or_property): 23 | """| Calls ``function_or_property`` if it's a function. 24 | | Gets ``function_or_property`` otherwise. 25 | 26 | In Flask-Login 0.2 ``is_authenticated`` and ``is_active`` were 27 | implemented as functions, while in 0.3+ they are implemented as properties. 28 | 29 | Example:: 30 | 31 | if self.call_or_get(current_user.is_authenticated): 32 | pass 33 | """ 34 | return function_or_property() if callable(function_or_property) else function_or_property 35 | 36 | def email_is_available(self, new_email): 37 | """Check if ``new_email`` is available. 38 | 39 | | Returns True if ``new_email`` does not exist or belongs to the current user. 40 | | Return False otherwise. 41 | """ 42 | 43 | user, user_email = self.db_manager.get_user_and_user_email_by_email(new_email) 44 | return (user == None) 45 | 46 | def generate_token(self, *args): 47 | """Convenience method that calls self.token_manager.generate_token(\*args).""" 48 | return self.token_manager.generate_token(*args) 49 | 50 | def hash_password(self, password): 51 | """Convenience method that calls self.password_manager.hash_password(password).""" 52 | return self.password_manager.hash_password(password) 53 | 54 | def make_safe_url(self, url): 55 | """Makes a URL safe by removing optional hostname and port. 56 | 57 | Example: 58 | 59 | | ``make_safe_url('https://hostname:80/path1/path2?q1=v1&q2=v2#fragment')`` 60 | | returns ``'/path1/path2?q1=v1&q2=v2#fragment'`` 61 | 62 | Override this method if you need to allow a list of safe hostnames. 63 | """ 64 | 65 | # Split the URL into scheme, netloc, path, query and fragment 66 | parts = list(urlsplit(url)) 67 | 68 | # Clear scheme and netloc and rebuild URL 69 | parts[0] = '' # Empty scheme 70 | parts[1] = '' # Empty netloc (hostname:port) 71 | safe_url = urlunsplit(parts) 72 | return safe_url 73 | 74 | def prepare_domain_translations(self): 75 | """Set domain_translations for current request context.""" 76 | from .translation_utils import domain_translations 77 | if domain_translations: 78 | domain_translations.as_default() 79 | 80 | def verify_password(self, password, password_hash): 81 | """Convenience method that calls self.password_manager.verify_password(password, password_hash). 82 | """ 83 | return self.password_manager.verify_password(password, password_hash) 84 | 85 | def verify_token(self, token, expiration_in_seconds=None): 86 | """Convenience method that calls self.token_manager.verify_token(token, expiration_in_seconds).""" 87 | return self.token_manager.verify_token(token, expiration_in_seconds) 88 | -------------------------------------------------------------------------------- /flask_user/user_mixin.py: -------------------------------------------------------------------------------- 1 | """This module implements the UserMixin class for Flask-User. 2 | This Mixin adds required methods to User data-model. 3 | """ 4 | 5 | from flask import current_app 6 | from flask_login import UserMixin as FlaskLoginUserMixin 7 | 8 | class UserMixin(FlaskLoginUserMixin): 9 | """ This class adds required methods to the User data-model. 10 | 11 | Example: 12 | class User(db.Model, UserMixin): 13 | ... 14 | """ 15 | 16 | def get_id(self): 17 | """Converts a User ID and parts of a User password hash to a token.""" 18 | 19 | # This function is used by Flask-Login to store a User ID securely as a browser cookie. 20 | # The last part of the password is included to invalidate tokens when password change. 21 | # user_id and password_ends_with are encrypted, timestamped and signed. 22 | # This function works in tandem with UserMixin.get_user_by_token() 23 | user_manager = current_app.user_manager 24 | 25 | user_id = self.id 26 | password_ends_with = '' if user_manager.USER_ENABLE_AUTH0 else self.password[-8:] 27 | user_token = user_manager.generate_token( 28 | user_id, # User ID 29 | password_ends_with, # Last 8 characters of user password 30 | ) 31 | # print("UserMixin.get_id: ID:", self.id, "token:", user_token) 32 | return user_token 33 | 34 | @classmethod 35 | def get_user_by_token(cls, token, expiration_in_seconds=None): 36 | # This function works in tandem with UserMixin.get_id() 37 | # Token signatures and timestamps are verified. 38 | # user_id and password_ends_with are decrypted. 39 | 40 | # Verifies a token and decrypts a User ID and parts of a User password hash 41 | user_manager = current_app.user_manager 42 | data_items = user_manager.verify_token(token, expiration_in_seconds) 43 | 44 | # Verify password_ends_with 45 | token_is_valid = False 46 | if data_items: 47 | 48 | # Load user by User ID 49 | user_id = data_items[0] 50 | password_ends_with = data_items[1] 51 | user = user_manager.db_manager.get_user_by_id(user_id) 52 | user_password = '' if user_manager.USER_ENABLE_AUTH0 else user.password[-8:] 53 | 54 | # Make sure that last 8 characters of user password matches 55 | token_is_valid = user and user_password==password_ends_with 56 | 57 | return user if token_is_valid else None 58 | 59 | def has_roles(self, *requirements): 60 | """ Return True if the user has all of the specified roles. Return False otherwise. 61 | 62 | has_roles() accepts a list of requirements: 63 | has_role(requirement1, requirement2, requirement3). 64 | 65 | Each requirement is either a role_name, or a tuple_of_role_names. 66 | role_name example: 'manager' 67 | tuple_of_role_names: ('funny', 'witty', 'hilarious') 68 | A role_name-requirement is accepted when the user has this role. 69 | A tuple_of_role_names-requirement is accepted when the user has ONE of these roles. 70 | has_roles() returns true if ALL of the requirements have been accepted. 71 | 72 | For example: 73 | has_roles('a', ('b', 'c'), d) 74 | Translates to: 75 | User has role 'a' AND (role 'b' OR role 'c') AND role 'd'""" 76 | 77 | # Translates a list of role objects to a list of role_names 78 | user_manager = current_app.user_manager 79 | role_names = user_manager.db_manager.get_user_roles(self) 80 | 81 | # has_role() accepts a list of requirements 82 | for requirement in requirements: 83 | if isinstance(requirement, (list, tuple)): 84 | # this is a tuple_of_role_names requirement 85 | tuple_of_role_names = requirement 86 | authorized = False 87 | for role_name in tuple_of_role_names: 88 | if role_name in role_names: 89 | # tuple_of_role_names requirement was met: break out of loop 90 | authorized = True 91 | break 92 | if not authorized: 93 | return False # tuple_of_role_names requirement failed: return False 94 | else: 95 | # this is a role_name requirement 96 | role_name = requirement 97 | # the user must have this role 98 | if not role_name in role_names: 99 | return False # role_name requirement failed: return False 100 | 101 | # All requirements have been met: return True 102 | return True 103 | 104 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Python packages 2 | bcrypt>=1.1 3 | cryptography==2.3 4 | passlib>=1.7.0 5 | 6 | # Flask & Flask Extensions 7 | Flask==1.0.2 8 | Flask-BabelEx==0.9.3 9 | Flask-Login==0.4.0 10 | Flask-Mail==0.9.1 11 | Flask-WTF==0.14.2 12 | 13 | # SQLAlchemy testing 14 | Flask-SQLAlchemy==2.2 15 | 16 | # Mongo DB testing 17 | #MongoEngine<0.11 # 0.11.0 dropped support for python 2.6 18 | #Flask-MongoEngine==0.9.3 19 | 20 | # EmailAdapter testing 21 | sendgrid==5.2.0 22 | Flask-Sendmail==0.1 23 | 24 | # Development packages 25 | pytest==3.0.5 26 | pytest-cov==2.5.1 27 | Sphinx==1.6.3 28 | tox==2.5.0 29 | twine==1.9.1 30 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | from example_apps.quickstart_app import create_app 2 | 3 | app = create_app() 4 | 5 | app.run(host='0.0.0.0', port=5000, debug=True) 6 | 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup 3 | 4 | __title__ = 'Flask-User' 5 | __description__ = 'Customizable User Authentication & User Management: Register, Confirm, Login, Change username/password, Forgot password and more.' 6 | __version__ = '1.0.2.3' 7 | __url__ = 'https://github.com/lingthio/Flask-User' 8 | __author__ = 'Ling Thio' 9 | __author_email__= 'ling.thio@gmail.com' 10 | __maintainer__ = 'Ling Thio' 11 | __license__ = 'MIT' 12 | __copyright__ = '(c) 2013 Ling Thio' 13 | 14 | 15 | # Load pytest and pytest-runner only when needed: 16 | needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) 17 | pytest_runner = ['pytest-runner'] if needs_pytest else [] 18 | 19 | 20 | # Read long description from README.rst file 21 | def load_readme(): 22 | with open('README.rst') as f: 23 | return f.read() 24 | 25 | 26 | setup( 27 | name=__title__, 28 | version=__version__, 29 | description=__description__, 30 | long_description=load_readme(), 31 | keywords='Flask User Authorization Account Management Registration Username Email Confirmation Forgot Reset Password Invitation', 32 | url=__url__, 33 | author=__author__, 34 | author_email=__author_email__, 35 | license=__license__, 36 | 37 | platforms='any', 38 | classifiers=[ 39 | 'Development Status :: 5 - Production/Stable', 40 | 'Environment :: Web Environment', 41 | 'Framework :: Flask', 42 | 'Intended Audience :: Developers', 43 | 'License :: OSI Approved :: MIT License', 44 | 'Natural Language :: Chinese (Simplified)', 45 | 'Natural Language :: Dutch', 46 | 'Natural Language :: English', 47 | 'Natural Language :: German', 48 | 'Natural Language :: Spanish', 49 | 'Natural Language :: Finnish', 50 | 'Natural Language :: French', 51 | 'Natural Language :: Italian', 52 | 'Natural Language :: Persian', 53 | 'Natural Language :: Polish', 54 | 'Natural Language :: Russian', 55 | 'Natural Language :: Slovak', 56 | 'Natural Language :: Swedish', 57 | 'Natural Language :: Turkish', 58 | 'Natural Language :: Ukrainian', 59 | 'Operating System :: OS Independent', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 2.7', 62 | 'Programming Language :: Python :: 3.4', 63 | 'Programming Language :: Python :: 3.5', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Programming Language :: Python :: 3.7', 66 | 'Programming Language :: Python :: 3.8', 67 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 68 | 'Topic :: Security', 69 | 'Topic :: Software Development', 70 | 'Topic :: Software Development :: Libraries', 71 | 'Topic :: Software Development :: Libraries :: Python Modules', 72 | ], 73 | 74 | packages=['flask_user'], 75 | include_package_data=True, # Tells setup to use MANIFEST.in 76 | zip_safe=False, # Do not zip as it will make debugging harder 77 | 78 | python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', # Python 2.7 and 3.4+ 79 | setup_requires=['Flask-Login',] + pytest_runner, 80 | install_requires=[ 81 | 'bcrypt>=2.0', 82 | 'cryptography>=1.6', 83 | 'Flask>=0.9', 84 | 'Flask-Login>=0.2', 85 | 'Flask-Mail>=0.9', 86 | 'Flask-SQLAlchemy>=1.0', 87 | 'Flask-WTF>=0.9', 88 | 'passlib>=1.7', 89 | ], 90 | tests_require=['pytest'], 91 | ) 92 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Test on the following Python versions. 3 | # Latest test on Python 2.7.17, 3.4.10, 3.5.9, 3.6.9, 3.7.5, 3.8.0. 4 | # Please keep this list in sync with `.travis.yml`. 5 | envlist = py27, py34, py35, py36, py37, py38 6 | skip_missing_interpreters = True 7 | 8 | toxworkdir=../builds/flask_user1/tox 9 | skipsdist=True 10 | 11 | 12 | [testenv] 13 | deps = -r{toxinidir}/requirements.txt 14 | 15 | # Run automated test suite 16 | commands= 17 | py.test flask_user/tests/ 18 | --------------------------------------------------------------------------------