Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rest api #302

Merged
merged 10 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ recursive-include examples *.py
recursive-include examples *.sh
recursive-include examples *.txt
recursive-include invenio_accounts *.html
recursive-include invenio_accounts *.txt
recursive-include invenio_accounts *.po *.pot *.mo
recursive-include invenio_accounts *.py
recursive-include tests *.py
2 changes: 2 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ login, logout, email confirmations etc. Here are some few of the possiblities:

.. autodata:: invenio_accounts.config.SECURITY_RESET_PASSWORD_TEMPLATE

.. autodata:: invenio_accounts.config.SECURITY_CHANGE_PASSWORD_TEMPLATE

.. autodata:: invenio_accounts.config.SECURITY_FORGOT_PASSWORD_TEMPLATE

.. autodata:: invenio_accounts.config.SECURITY_SEND_CONFIRMATION_TEMPLATE
Expand Down
6 changes: 0 additions & 6 deletions examples/app-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ mkdir static

# Install specific dependencies
pip install -r requirements.txt
npm install -g [email protected] [email protected] [email protected] [email protected]

# Install assets
flask npm
cd static
npm install
cd ..
flask collect
flask webpack buildall

Expand Down
42 changes: 42 additions & 0 deletions invenio_accounts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@
ACCOUNTS_SETTINGS_SECURITY_TEMPLATE = 'invenio_accounts/settings/security.html'
"""Template for the account security page."""

ACCOUNTS_CONFIRM_EMAIL_ENDPOINT = None
"""Value to be used for the confirmation email link in the UI application."""

ACCOUNTS_REST_CONFIRM_EMAIL_ENDPOINT = '/confirm/{token}'
"""Value to be used for the confirmation email link in the API application.

Can be a Flask endpoint (e.g. "invenio_accounts_rest_auth.confirm_email"), or
a URL part (e.g. "https://ui.example.com/confirm-email", "/confirm-email").

This will be used to build an absolute URL, thus if e.g. a hostname isn't
included, the one from the current request's context will be used.
"""

ACCOUNTS_RESET_PASSWORD_ENDPOINT = None
"""Value to be used for the confirmation email link in the UI application."""

ACCOUNTS_REST_RESET_PASSWORD_ENDPOINT = '/lost-password/{token}'
"""Value to be used for the reset password link in the API application.

Can be a Flask endpoint (e.g. "invenio_accounts_rest_auth.reset_password"), or
a URL part (e.g. "https://ui.example.com/reset-password", "/reset-password").

This will be used to build an absolute URL, thus if e.g. a hostname isn't
included, the one from the current request's context will be used.
"""

ACCOUNTS_REST_AUTH_VIEWS = {
"login": "invenio_accounts.views.rest:LoginView",
"logout": "invenio_accounts.views.rest:LogoutView",
"user_info": "invenio_accounts.views.rest:UserInfoView",
"register": "invenio_accounts.views.rest:RegisterView",
"forgot_password": "invenio_accounts.views.rest:ForgotPasswordView",
"reset_password": "invenio_accounts.views.rest:ResetPasswordView",
"change_password": "invenio_accounts.views.rest:ChangePasswordView",
"send_confirmation":
"invenio_accounts.views.rest:SendConfirmationEmailView",
"confirm_email": "invenio_accounts.views.rest:ConfirmEmailView",
"sessions_list": "invenio_accounts.views.rest:SessionsListView",
"sessions_item": "invenio_accounts.views.rest:SessionsItemView"
}
"""List of REST API authentication views."""

# Change Flask-Security defaults
SECURITY_PASSWORD_HASH = 'pbkdf2_sha512'
"""Default password hashing algorithm for new passwords."""
Expand Down
25 changes: 23 additions & 2 deletions invenio_accounts/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import pkg_resources
import six
from flask import current_app, request_finished, session
from flask import Blueprint, abort, current_app, request_finished, session
from flask_kvsession import KVSessionExtension
from flask_login import LoginManager, user_logged_in, user_logged_out
from flask_principal import AnonymousIdentity
Expand Down Expand Up @@ -264,10 +264,31 @@ def init_app(self, app, sessionstore=None, register_blueprint=False):
:param register_blueprint: If ``True``, the application registers the
blueprints. (Default: ``True``)
"""
return super(InvenioAccountsREST, self).init_app(
# Register the Flask-Security blueprint for the email templates
if not register_blueprint:
security_bp = Blueprint(
'security_email_templates', # name differently to avoid misuse
'flask_security.core', template_folder='templates')
security_rest_overrides = Blueprint(
'security_email_overrides', # overrides
__name__, template_folder='templates')
app.register_blueprint(security_bp)
app.register_blueprint(security_rest_overrides)

super(InvenioAccountsREST, self).init_app(
app, sessionstore=sessionstore,
register_blueprint=register_blueprint,
)
app.config['ACCOUNTS_CONFIRM_EMAIL_ENDPOINT'] = \
app.config['ACCOUNTS_REST_CONFIRM_EMAIL_ENDPOINT']
app.config['ACCOUNTS_RESET_PASSWORD_ENDPOINT'] = \
app.config['ACCOUNTS_REST_RESET_PASSWORD_ENDPOINT']

if app.config.get("ACCOUNTS_REGISTER_UNAUTHORIZED_CALLBACK", True):
def _unauthorized_callback():
"""Callback to abort when user is unauthorized."""
abort(401)
app.login_manager.unauthorized_handler(_unauthorized_callback)


class InvenioAccountsUI(InvenioAccounts):
Expand Down
7 changes: 7 additions & 0 deletions invenio_accounts/proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@
lambda: current_app.extensions['invenio-accounts']
)
"""Proxy to the current Invenio-Accounts extension."""

current_security = LocalProxy(lambda: current_app.extensions['security'])
"""Proxy to the Flask-Security extension."""

current_datastore = LocalProxy(
lambda: current_app.extensions['security'].datastore)
"""Proxy to the current Flask-Security user datastore."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>{{ _('Your password has been changed.') }}</p>
{% if security.recoverable %}
<p>{{ _('If you did not change your password,') }} <a href="{{ reset_password_link }}">{{ _('click here to reset it') }}</a>.</p>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{ _('Your password has been changed') }}
{% if security.recoverable %}
{{ _('If you did not change your password, click the link below to reset it.') }}
{{ reset_password_link }}
{% endif %}

2 changes: 1 addition & 1 deletion invenio_accounts/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def create_test_user(email, password='123456', **kwargs):
:returns: A :class:`invenio_accounts.models.User` instance.
"""
assert flask.current_app.testing
hashed_password = hash_password(password)
hashed_password = hash_password(password) if password else None
user = _datastore.create_user(email=email, password=hashed_password,
**kwargs)
_datastore.commit()
Expand Down
86 changes: 85 additions & 1 deletion invenio_accounts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@
from datetime import datetime

import six
from flask import current_app, session
from flask import current_app, request, session, url_for
from flask_security import current_user
from flask_security.confirmable import generate_confirmation_token
from flask_security.recoverable import generate_reset_password_token
from flask_security.signals import password_changed, user_registered
from flask_security.utils import config_value as security_config_value
from flask_security.utils import get_security_endpoint_name, hash_password, \
send_mail
from future.utils import raise_from
from jwt import DecodeError, ExpiredSignatureError, decode, encode
from six.moves.urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
from werkzeug.routing import BuildError
from werkzeug.utils import import_string

from .errors import JWTDecodeError, JWTExpiredToken
from .proxies import current_datastore, current_security


def jwt_create_token(user_id=None, additional_data=None):
Expand Down Expand Up @@ -101,3 +110,78 @@ def obj_or_import_string(value, default=None):
elif value:
return value
return default


def _generate_token_url(endpoint, token):
try:
url = url_for(endpoint, token=token, _external=True)
except BuildError:
# Try to parse URL and build
scheme, netloc, path, query, fragment = urlsplit(endpoint)
scheme = scheme or request.scheme
netloc = netloc or request.host
assert netloc
qs = parse_qs(query)
if '{token}' in path:
path = path.format(token=token)
else:
qs['token'] = token
query = urlencode(qs)
url = urlunsplit((scheme, netloc, path, query, fragment))
return url


def default_reset_password_link_func(user):
"""Return the reset password link that will be sent to a user via email."""
token = generate_reset_password_token(user)
endpoint = current_app.config['ACCOUNTS_RESET_PASSWORD_ENDPOINT'] or \
get_security_endpoint_name('reset_password')
return token, _generate_token_url(endpoint, token)


def default_confirmation_link_func(user):
"""Return the confirmation link that will be sent to a user via email."""
token = generate_confirmation_token(user)
endpoint = current_app.config['ACCOUNTS_CONFIRM_EMAIL_ENDPOINT'] or \
get_security_endpoint_name('confirm_email')
return token, _generate_token_url(endpoint, token)


def register_user(_confirmation_link_func=None, **user_data):
"""Register a user."""
confirmation_link_func = _confirmation_link_func or \
default_confirmation_link_func
user_data['password'] = hash_password(user_data['password'])
user = current_datastore.create_user(**user_data)
current_datastore.commit()

token, confirmation_link = None, None
if current_security.confirmable and user.confirmed_at is None:
token, confirmation_link = confirmation_link_func(user)

user_registered.send(
current_app._get_current_object(), user=user, confirm_token=token)

if security_config_value('SEND_REGISTER_EMAIL'):
send_mail(security_config_value('EMAIL_SUBJECT_REGISTER'), user.email,
'welcome', user=user, confirmation_link=confirmation_link)

return user


def change_user_password(_reset_password_link_func=None, **user_data):
"""Change user password."""
reset_password_link_func = _reset_password_link_func or \
default_reset_password_link_func
user = user_data['user']
user.password = hash_password(user_data['password'])
current_datastore.put(user)
if security_config_value('SEND_PASSWORD_CHANGE_EMAIL'):
reset_password_link = None
if current_security.recoverable:
_, reset_password_link = reset_password_link_func(user)
subject = security_config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE')
send_mail(subject, user.email, 'change_notice_rest', user=user,
reset_password_link=reset_password_link)
password_changed.send(current_app._get_current_object(),
user=user)
2 changes: 1 addition & 1 deletion invenio_accounts/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@

from .settings import blueprint

__all__ = ('blueprint', )
__all__ = ('blueprint')
Loading