diff --git a/.gitignore b/.gitignore index 7d8d699..894a44c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,35 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +*$py.class # C extensions *.so # Distribution / packaging .Python -env/ -bin/ build/ develop-eggs/ dist/ +downloads/ eggs/ +.eggs/ lib/ lib64/ parts/ sdist/ var/ +wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec # Installer logs pip-log.txt @@ -30,28 +39,66 @@ pip-delete-this-directory.txt htmlcov/ .tox/ .coverage +.coverage.* .cache nosetests.xml coverage.xml -coverage_html +*.cover +.hypothesis/ +.pytest_cache/ -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -.ropeproject +# Translations +*.mo +*.pot # Django stuff: *.log -*.pot +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ -.DS_Store -db.sqlite3 +# PyBuilder +target/ -# IntelliJ IDE files -.idea +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml index 7b4f050..b059c1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,14 @@ language: python python: - "2.7" - "3.5" + - "3.6" env: - - DJANGO=1.8.17 - - DJANGO=1.9.12 - - DJANGO=1.10.4 + - DJANGO=1.11.* DRF=3.7.* + - DJANGO=1.11.* DRF=3.8.* + - DJANGO=2.0.* DRF=3.7.* + - DJANGO=2.0.* DRF=3.8.* install: - - pip install -q Django==$DJANGO + - pip install -q Django==$DJANGO djangorestframework==$DRF - pip install coveralls - pip install -r rest_auth/tests/requirements.pip script: @@ -16,3 +18,9 @@ after_success: - coveralls before_script: - flake8 . --config=flake8 +matrix: + exclude: + - python: "2.7" + env: DJANGO=2.0.* DRF=3.7.* + - python: "2.7" + env: DJANGO=2.0.* DRF=3.8.* diff --git a/MANIFEST.in b/MANIFEST.in index a24b46a..01a589f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include AUTHORS include LICENSE include MANIFEST.in include README.md +graft rest_auth diff --git a/README.rst b/README.rst index 9014187..9b54d48 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,13 @@ Welcome to django-rest-auth =========================== -.. image:: https://travis-ci.org/Tivix/django-rest-auth.png +Repository is unmaintained at the moment (on pause). More info can be found on this issue page: https://github.com/Tivix/django-rest-auth/issues/568 + +.. image:: https://travis-ci.org/Tivix/django-rest-auth.svg :target: https://travis-ci.org/Tivix/django-rest-auth -.. image:: https://coveralls.io/repos/Tivix/django-rest-auth/badge.png +.. image:: https://coveralls.io/repos/Tivix/django-rest-auth/badge.svg :target: https://coveralls.io/r/Tivix/django-rest-auth?branch=master @@ -28,4 +30,4 @@ https://github.com/Tivix/django-rest-auth Stack Overflow ----------- -http://stackoverflow.com/questions/tagged/django-rest-auth \ No newline at end of file +http://stackoverflow.com/questions/tagged/django-rest-auth diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 7d6399d..514a8fb 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -47,16 +47,18 @@ INSTALLED_APPS = ( 'rest_framework_swagger', ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) +# For backwards compatibility for Django 1.8 +MIDDLEWARE_CLASSES = MIDDLEWARE + ROOT_URLCONF = 'demo.urls' WSGI_APPLICATION = 'demo.wsgi.application' diff --git a/demo/demo/urls.py b/demo/demo/urls.py index e66bc9d..af7d38b 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -38,7 +38,7 @@ urlpatterns = [ url(r'^rest-auth/', include('rest_auth.urls')), url(r'^rest-auth/registration/', include('rest_auth.registration.urls')), url(r'^account/', include('allauth.urls')), - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), url(r'^docs/$', get_swagger_view(title='API Docs'), name='api_docs') ] diff --git a/demo/requirements.pip b/demo/requirements.pip index 2fd3e39..40b4820 100644 --- a/demo/requirements.pip +++ b/demo/requirements.pip @@ -1,6 +1,6 @@ -django>=1.8.0 -django-rest-auth==0.9.1 -djangorestframework==3.5.1 +django>=1.9.0 +django-rest-auth==0.9.5 +djangorestframework>=3.7.0 django-allauth>=0.24.1 six==1.9.0 django-rest-swagger==2.0.7 diff --git a/dev-requirements.txt b/dev-requirements.txt index b0a8d61..0a2a9e4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ --editable . responses>=0.5.0 djangorestframework-jwt +django-allauth diff --git a/docs/changelog.rst b/docs/changelog.rst index ff2f542..e7beb5d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,31 @@ Changelog ========= +0.9.5 +----- +- fixed package distribution issue + +0.9.4 +----- +- Compatibility fixes (#437, #506) +- JWT auth cookie fix (#345) +- config & packaging fixes +- updated docs +- added new translations (Czech, Chinese, Turkish, Korean) + +0.9.3 +----- +- added social connect views +- added check for pre-existing accounts in social login +- prevent double-validation in LoginSerializer +- unit tests and demo project changes for Django 2.0 + +0.9.2 +----- +- added permission classes configuration for registration +- added more info to JWT docs +- added Polish translations + 0.9.1 ----- - fixed import error when extending rest_auth serializers diff --git a/docs/conf.py b/docs/conf.py index faba401..1c825cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,16 +44,16 @@ master_doc = 'index' # General information about the project. project = u'django-rest-auth' -copyright = u'2014, Tivix Inc.' +copyright = u'2018, Tivix Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.3.0' +version = '0.9.5' # The full version, including alpha/beta/rc tags. -release = '0.3.0' +release = '0.9.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/configuration.rst b/docs/configuration.rst index 117b691..b3e62ac 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -36,7 +36,7 @@ Configuration You can define your custom serializers for registration endpoint. Possible key values: - - REGISTER_SERIALIZER - serializer class in ``rest_auth.register.views.RegisterView``, default value ``rest_auth.registration.serializers.RegisterSerializer`` + - REGISTER_SERIALIZER - serializer class in ``rest_auth.registration.views.RegisterView``, default value ``rest_auth.registration.serializers.RegisterSerializer`` .. note:: The custom REGISTER_SERIALIZER must define a ``def save(self, request)`` method that returns a user model instance diff --git a/docs/installation.rst b/docs/installation.rst index 6144011..b382d43 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -26,12 +26,18 @@ Installation .. code-block:: python - urlpatterns = patterns('', + urlpatterns = [ ..., url(r'^rest-auth/', include('rest_auth.urls')) - ) + ] +4. Migrate your database +.. code-block:: python + + python manage.py migrate + + You're good to go now! @@ -60,11 +66,11 @@ Registration (optional) .. code-block:: python - urlpatterns = patterns('', + urlpatterns = [ ..., url(r'^rest-auth/', include('rest_auth.urls')), url(r'^rest-auth/registration/', include('rest_auth.registration.urls')) - ) + ] Social Authentication (optional) @@ -114,10 +120,10 @@ Facebook .. code-block:: python - urlpatterns += patterns('', + urlpatterns += [ ..., url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') - ) + ] Twitter @@ -125,15 +131,15 @@ Twitter If you are using Twitter for your social authentication, it is a bit different since Twitter uses OAuth 1.0. -3. Create new view as a subclass of ``rest_auth.views.LoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute: +3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute: .. code-block:: python from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter - from rest_auth.views import LoginView + from rest_auth.registration.views import SocialLoginView from rest_auth.social_serializers import TwitterLoginSerializer - class TwitterLogin(LoginView): + class TwitterLogin(SocialLoginView): serializer_class = TwitterLoginSerializer adapter_class = TwitterOAuthAdapter @@ -141,23 +147,114 @@ If you are using Twitter for your social authentication, it is a bit different s .. code-block:: python - urlpatterns += patterns('', + urlpatterns += [ ..., url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') - ) + ] + .. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details. -JWT Support (optional) ----------------------- +GitHub +###### -By default, ``django-rest-auth`` uses Django's Token-based authentication. If you want to use JWT authentication, you need to install the following: +If you are using GitHub for your social authentication, it uses code and not AccessToken directly. -1. Install ``django-rest-framework-jwt`` http://getblimp.github.io/django-rest-framework-jwt/ . Right now this is the only supported JWT library. +3. Create new view as a subclass of ``rest_auth.views.SocialLoginView`` with ``GitHubOAuth2Adapter`` adapter, an ``OAuth2Client`` and a callback_url as attributes: -2. Add the following to your settings +.. code-block:: python + + from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter + from allauth.socialaccount.providers.oauth2.client import OAuth2Client + from rest_auth.registration.views import SocialLoginView + + class GithubLogin(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + callback_url = CALLBACK_URL_YOU_SET_ON_GITHUB + client_class = OAuth2Client + +4. Create url for GitHubLogin view: + +.. code-block:: python + + urlpatterns += [ + ..., + url(r'^rest-auth/github/$', GitHubLogin.as_view(), name='github_login') + ] + +Additional Social Connect Views +############################### + +If you want to allow connecting existing accounts in addition to login, you can use connect views: + +.. code-block:: python + + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter + from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter + from allauth.socialaccount.providers.oauth2.client import OAuth2Client + from rest_auth.registration.views import SocialConnectView + from rest_auth.social_serializers import TwitterConnectSerializer + + class FacebookConnect(SocialConnectView): + adapter_class = FacebookOAuth2Adapter + + class TwitterConnect(SocialConnectView): + serializer_class = TwitterConnectSerializer + adapter_class = TwitterOAuthAdapter + + class GithubConnect(SocialConnectView): + adapter_class = GitHubOAuth2Adapter + callback_url = CALLBACK_URL_YOU_SET_ON_GITHUB + client_class = OAuth2Client + + +In urls.py: + +.. code-block:: python + + urlpatterns += [ + ..., + url(r'^rest-auth/facebook/connect/$', FacebookConnect.as_view(), name='fb_connect') + url(r'^rest-auth/twitter/connect/$', TwitterConnect.as_view(), name='twitter_connect') + url(r'^rest-auth/github/connect/$', GithubConnect.as_view(), name='github_connect') + ] + +You can also use the following views to check all social accounts attached to the current authenticated user and disconnect selected social accounts: + +.. code-block:: python + + from rest_auth.registration.views import ( + SocialAccountListView, SocialAccountDisconnectView + ) + + urlpatterns += [ + ..., + url( + r'^socialaccounts/$', + SocialAccountListView.as_view(), + name='social_account_list' + ), + url( + r'^socialaccounts/(?P\d+)/disconnect/$', + SocialAccountDisconnectView.as_view(), + name='social_account_disconnect' + ) + ] + + +JSON Web Token (JWT) Support (optional) +--------------------------------------- + +By default ``django-rest-auth`` uses Django's Token-based authentication. If you want to use JWT authentication, follow these steps: + +1. Install `djangorestframework-jwt `_ + - ``djangorestframework-jwt`` is currently the only supported JWT library. +2. The ``JWT_PAYLOAD_HANDLER`` and ``JWT_ENCODE_HANDLER`` settings are imported from the ``django-rest-framework-jwt`` settings object. + - Refer to `the library's documentation `_ for information on using different encoders. + +3. Add the following configuration value to your settings file to enable JWT authentication. .. code-block:: python REST_USE_JWT = True - diff --git a/flake8 b/flake8 deleted file mode 100644 index 401fdf4..0000000 --- a/flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = docs/*,demo/* -ignore = F403 \ No newline at end of file diff --git a/rest_auth/locale/cs/LC_MESSAGES/django.mo b/rest_auth/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 0000000..3dd71df Binary files /dev/null and b/rest_auth/locale/cs/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/cs/LC_MESSAGES/django.po b/rest_auth/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 0000000..eb8c60c --- /dev/null +++ b/rest_auth/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,102 @@ +# Czech translations of Tivix/django-rest-auth +# +# This file is distributed under the same license as the Tivix/django-rest-auth package. +# +msgid "" +msgstr "" +"Project-Id-Version: Tivix/django-rest-auth\n" +"Report-Msgid-Bugs-To: https://github.com/Tivix/django-rest-auth/issues\n" +"POT-Creation-Date: 2018-06-27 23:05+0200\n" +"PO-Revision-Date: 2018-06-27 23:22+0200\n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"Last-Translator: Václav Dohnal \n" +"Language-Team: N/A\n" +"X-Generator: Poedit 2.0.8\n" + +#: .\registration\serializers.py:67 +msgid "View is not defined, pass it as a context variable" +msgstr "View není definováno, předejte jej jako proměnnou kontextu" + +#: .\registration\serializers.py:72 +msgid "Define adapter_class in view" +msgstr "Definujte adapter_class ve view" + +#: .\registration\serializers.py:91 +msgid "Define callback_url in view" +msgstr "Definujte callback_url ve view" + +#: .\registration\serializers.py:95 +msgid "Define client_class in view" +msgstr "Definujte client_class ve view" + +#: .\registration\serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "Nesprávný vstup. access_token je povinný." + +#: .\registration\serializers.py:125 +msgid "Incorrect value" +msgstr "Nesprávná hodnota" + +#: .\registration\serializers.py:139 +msgid "User is already registered with this e-mail address." +msgstr "Uživatel s touto adresou je již registrován." + +#: .\registration\serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "Uživatel s touto adresou je již registrován." + +#: .\registration\serializers.py:193 +msgid "The two password fields didn't match." +msgstr "Zadaná hesla se neshodují." + +#: .\registration\views.py:51 +msgid "Verification e-mail sent." +msgstr "Ověřovací e-mail odeslán." + +#: .\registration\views.py:98 +msgid "ok" +msgstr "ok" + +#: .\serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "Musí obsahovat \"e-mail\" a \"heslo\"." + +#: .\serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "Musí obsahovat \"uživatelské jméno\" a \"heslo\"." + +#: .\serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Musí obsahovat \"uživatelské jméno\" nebo \"e-mail\" a \"heslo\"." + +#: .\serializers.py:95 +msgid "User account is disabled." +msgstr "Uživatelský účet je zakázán." + +#: .\serializers.py:98 +msgid "Unable to log in with provided credentials." +msgstr "Pomocí zadaných údajů se nelze přihlásit." + +#: .\serializers.py:107 +msgid "E-mail is not verified." +msgstr "E-mail není ověřený." + +#: .\views.py:127 +msgid "Successfully logged out." +msgstr "Byli jste úspěšně odhlášeni." + +#: .\views.py:175 +msgid "Password reset e-mail has been sent." +msgstr "E-mail pro resetování hesla byl odeslán." + +#: .\views.py:201 +msgid "Password has been reset with the new password." +msgstr "Vaše heslo bylo resetováno." + +#: .\views.py:223 +msgid "New password has been saved." +msgstr "Nové heslo bylo uloženo." diff --git a/rest_auth/locale/fr/LC_MESSAGES/django.mo b/rest_auth/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..510118c Binary files /dev/null and b/rest_auth/locale/fr/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/fr/LC_MESSAGES/django.po b/rest_auth/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..b1e97d3 --- /dev/null +++ b/rest_auth/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,98 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-12-22 11:37-0800\n" +"PO-Revision-Date: 2017-02-14 13:27+0100\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.11\n" + +#: registration/serializers.py:53 +msgid "View is not defined, pass it as a context variable" +msgstr "La “View” n’est pas définie, passez la en variable contextuelle" + +#: registration/serializers.py:58 +msgid "Define adapter_class in view" +msgstr "Définissez “adapter_class” dans la vue" + +#: registration/serializers.py:77 +msgid "Define callback_url in view" +msgstr "Définissez “callback_url” dans la vue" + +#: registration/serializers.py:81 +msgid "Define client_class in view" +msgstr "Définissez “client_class” dans la vue" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "Paramètres incorrects. Il faut “access_token” ou “code”." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "Paramètre incorrect" + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "Un utilisateur existe déjà avec cette adresse email." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Les deux mots de passes ne sont pas les mêmes." + +#: registration/views.py:82 +msgid "ok" +msgstr "Ok" + +#: serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "Doit inclure “email” et “password”." + +#: serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "Doit inclure “username” et “password”." + +#: serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Doit inclure un “username” ou “email”, et un “password”." + +#: serializers.py:95 +msgid "User account is disabled." +msgstr "Le compte utilisateur est désactivé." + +#: serializers.py:98 +msgid "Unable to log in with provided credentials." +msgstr "Connexion impossible avec les informations fournies." + +#: serializers.py:107 +msgid "E-mail is not verified." +msgstr "L’adresse email n’a pas été vérifiée." + +#: views.py:114 +msgid "Successfully logged out." +msgstr "Déconnexion effectuée avec succès." + +#: views.py:162 +msgid "Password reset e-mail has been sent." +msgstr "L’email de réinitialisation du mot de passe a été envoyé." + +#: views.py:184 +msgid "Password has been reset with the new password." +msgstr "Le mot de passe a été réinitialisé." + +#: views.py:202 +msgid "New password has been saved." +msgstr "Le nouveau mot de passe est sauvé." + +#~ msgid "Error" +#~ msgstr "Fehler" diff --git a/rest_auth/locale/ko/LC_MESSAGES/django.mo b/rest_auth/locale/ko/LC_MESSAGES/django.mo new file mode 100644 index 0000000..79c07cf Binary files /dev/null and b/rest_auth/locale/ko/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/ko/LC_MESSAGES/django.po b/rest_auth/locale/ko/LC_MESSAGES/django.po new file mode 100644 index 0000000..777a2bf --- /dev/null +++ b/rest_auth/locale/ko/LC_MESSAGES/django.po @@ -0,0 +1,99 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-05 21:56-0800\n" +"PO-Revision-Date: 2018-03-20 17:52+0900\n" +"Last-Translator: Jeonsgoo Park \n" +"Language-Team: LANGUAGE \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: registration/serializers.py:53 +msgid "View is not defined, pass it as a context variable" +msgstr "View가 정의되지 않았습니다. 컨텍스트 변수에 포함해주세요" + +#: registration/serializers.py:58 +msgid "Define adapter_class in view" +msgstr "view에 adapter_class를 정의하세요" + +#: registration/serializers.py:77 +msgid "Define callback_url in view" +msgstr "view에 callback_url을 정의하세요" + +#: registration/serializers.py:81 +msgid "Define client_class in view" +msgstr "view에 client_class를 정의하세요" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "올바르지 않은 입력입니다. access_token이나 code가 필요합니다." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "올바르지 않은 값" + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "이미 이 이메일 주소로 등록된 사용자가 있습니다." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "두 개의 패스워드 필드가 서로 맞지 않습니다." + +#: registration/views.py:44 +msgid "Verification e-mail sent." +msgstr "확인 이메일을 발송했습니다." + +#: registration/views.py:91 +msgid "ok" +msgstr "ok" + +#: serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "\"email\"과 \"password\"를 반드시 포함해야 합니다." + +#: serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "\"username\"과 \"password\"를 반드시 포함해야 합니다." + +#: serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "\"username\"이나 \"email\", 그리고 \"password\"를 반드시 포함해야 합니다." + +#: serializers.py:95 +msgid "User account is disabled." +msgstr "사용자 계정이 비활성화 되어있습니다." + +#: serializers.py:98 +msgid "Unable to log in with provided credentials." +msgstr "주어진 자격 증명으로 로그인이 불가능합니다." + +#: serializers.py:107 +msgid "E-mail is not verified." +msgstr "이메일 주소가 확인되지 않았습니다." + +#: views.py:126 +msgid "Successfully logged out." +msgstr "로그아웃되었습니다." + +#: views.py:174 +msgid "Password reset e-mail has been sent." +msgstr "패스워드 초기화 이메일이 발송되었습니다." + +#: views.py:200 +msgid "Password has been reset with the new password." +msgstr "새로운 패스워드로 패스워드가 초기화 되었습니다." + +#: views.py:222 +msgid "New password has been saved." +msgstr "새로운 패스워드가 저장되었습니다." diff --git a/rest_auth/locale/pl/LC_MESSAGES/django.mo b/rest_auth/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..bee2ff6 Binary files /dev/null and b/rest_auth/locale/pl/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/pl/LC_MESSAGES/django.po b/rest_auth/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..815dac0 --- /dev/null +++ b/rest_auth/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,99 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-05 21:56-0800\n" +"PO-Revision-Date: 2017-03-11 12:10+0100\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Language-Team: \n" +"X-Generator: Poedit 1.8.11\n" +"Last-Translator: \n" +"Language: pl\n" + +#: registration/serializers.py:53 +msgid "View is not defined, pass it as a context variable" +msgstr "Widok nie został zdefiniowany, przekaż go przez zmienną \"context\"" + +#: registration/serializers.py:58 +msgid "Define adapter_class in view" +msgstr "Zdefiniuj \"adapter_class\" w widoku" + +#: registration/serializers.py:77 +msgid "Define callback_url in view" +msgstr "Zdefiniuj \"callback_url\" w widoku" + +#: registration/serializers.py:81 +msgid "Define client_class in view" +msgstr "Zdefiniuj \"client_class\" w widoku" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "Podano błędne dane. \"access_token\" lub \"code\" są wymagne." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "Niepoprawna wartość." + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "Istnieje już użytkownik z takim adresem email." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Hasła nie są identyczne." + +#: registration/views.py:44 +msgid "Verification e-mail sent." +msgstr "Email weryfikacyjny został wysłany." + +#: registration/views.py:91 +msgid "ok" +msgstr "ok" + +#: serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "Musisz podać email i hasło." + +#: serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "Musisz podać nazwę użytkownika i hasło." + +#: serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Musisz podać nazwę użytkownika (lub email) i hasło." + +#: serializers.py:95 +msgid "User account is disabled." +msgstr "Konto użytkownika zostało wyłączone." + +#: serializers.py:98 +msgid "Unable to log in with provided credentials." +msgstr "Podane dane do logowania są niepoprawne." + +#: serializers.py:107 +msgid "E-mail is not verified." +msgstr "Email nie został zweryfikowany." + +#: views.py:126 +msgid "Successfully logged out." +msgstr "Wylogowano." + +#: views.py:174 +msgid "Password reset e-mail has been sent." +msgstr "Email z linkiem do resetu hasła został wysłany." + +#: views.py:200 +msgid "Password has been reset with the new password." +msgstr "Hasło zostało zresetowane." + +#: views.py:222 +msgid "New password has been saved." +msgstr "Nowe hasło zostało zapisane." diff --git a/rest_auth/locale/pt_BR/LC_MESSAGES/django.po b/rest_auth/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000..d816a54 --- /dev/null +++ b/rest_auth/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,99 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Bruno Barreto Freitas , 2019. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-04-16 09:48-0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Bruno Barreto Freitas \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: registration/serializers.py:53 +msgid "View is not defined, pass it as a context variable" +msgstr "\"View\" não está definida, passe-a como uma variável de contexto" + +#: registration/serializers.py:58 +msgid "Define adapter_class in view" +msgstr "Defina \"adapter_class\" na view" + +#: registration/serializers.py:77 +msgid "Define callback_url in view" +msgstr "Defina \"callback_url\" na view" + +#: registration/serializers.py:81 +msgid "Define client_class in view" +msgstr "Defina \"client_class\" na view" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "Entrada incorreta. \"access_token\" ou \"code\" são obrigatórios." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "Valor incorreto" + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "Já existe um usuário cadastrado com este endereço de e-mail." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Os dois campos de senha não correspondem." + +#: registration/views.py:44 +msgid "Verification e-mail sent." +msgstr "E-mail de verificação enviado." + +#: registration/views.py:91 +msgid "ok" +msgstr "Ok" + +#: serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "Deve-se incluir \"email\" e \"password\"." + +#: serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "Deve-se incluir \"username\" e \"password\"." + +#: serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Deve-se incluir \"username\" ou \"email\" e \"password\"." + +#: serializers.py:95 +msgid "User account is disabled." +msgstr "Conta de usuário está desativada" + +#: serializers.py:98 +msgid "Unable to log in with provided credentials." +msgstr "Não foi possível realizar o login com as credenciais fornecidas." + +#: serializers.py:107 +msgid "E-mail is not verified." +msgstr "E-mail não foi verificado." + +#: views.py:126 +msgid "Successfully logged out." +msgstr "Logout realizado com sucesso." + +#: views.py:174 +msgid "Password reset e-mail has been sent." +msgstr "E-mail de redefinição de senha foi enviado." + +#: views.py:200 +msgid "Password has been reset with the new password." +msgstr "Senha foi redefinida com a nova senha." + +#: views.py:222 +msgid "New password has been saved." +msgstr "Nova senha foi salva com sucesso." diff --git a/rest_auth/locale/tr/LC_MESSAGES/django.po b/rest_auth/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 0000000..f02af80 --- /dev/null +++ b/rest_auth/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,95 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-05 21:56-0800\n" +"PO-Revision-Date: 2018-10-13 19:37+0300\n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Generator: Poedit 2.2\n" + +#: registration/serializers.py:67 +msgid "View is not defined, pass it as a context variable" +msgstr "“View” tanımlanmadı, “context” değişkeni olarak tanımla" + +#: registration/serializers.py:72 +msgid "Define adapter_class in view" +msgstr "“view” içerisinde “adapter_class” tanımla" + +#: registration/serializers.py:91 +msgid "Define callback_url in view" +msgstr "“view” içerisinde “callback_url” tanımla" + +#: registration/serializers.py:95 +msgid "Define client_class in view" +msgstr "“view” içerisinde “client_class” tanımla" + +#: registration/serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "Geçersiz girdi. “access_token” veya “code” gerekli." + +#: registration/serializers.py:125 +msgid "Incorrect value" +msgstr "Geçersiz değer" + +#: registration/serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "Bu e-posta adresi ile bir kullanıcı zaten kayıt olmuştu." + +#: registration/serializers.py:193 +msgid "The two password fields didn't match." +msgstr "İki şifre alanı eşleşmiyor." + +#: registration/views.py:98 +msgid "ok" +msgstr "tamam" + +#: serializers.py:33 +msgid "Must include \"email\" and \"password\"." +msgstr "\"email\" ve \"password\" içermelidir." + +#: serializers.py:44 +msgid "Must include \"username\" and \"password\"." +msgstr "“username\" und \"password\" içermelidir." + +#: serializers.py:57 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Ya ”username\" yada \"email\" ve \"password\" içermelidir." + +#: serializers.py:98 +msgid "User account is disabled." +msgstr "Kullanıcı hesap pasiftir." + +#: serializers.py:101 +msgid "Unable to log in with provided credentials." +msgstr "Sağlanan kimlik bilgileri ile giriş yapılamıyor." + +#: serializers.py:110 +msgid "E-mail is not verified." +msgstr "E-posta adresi doğrulanmadı." + +#: views.py:127 +msgid "Successfully logged out." +msgstr "Başarılı bir şekilde çıkış yapıldı." + +#: views.py:175 +msgid "Password reset e-mail has been sent." +msgstr "Şifre sıfırlama e-postası gönderildi." + +#: views.py:201 +msgid "Password has been reset with the new password." +msgstr "Yeni şifre ile şifre sıfırlandı." + +#: views.py:223 +msgid "New password has been saved." +msgstr "Yeni şifre kaydedildi." diff --git a/rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo b/rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo new file mode 100644 index 0000000..2f6f549 Binary files /dev/null and b/rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/zh_Hans/LC_MESSAGES/django.po b/rest_auth/locale/zh_Hans/LC_MESSAGES/django.po new file mode 100644 index 0000000..1c7ff82 --- /dev/null +++ b/rest_auth/locale/zh_Hans/LC_MESSAGES/django.po @@ -0,0 +1,104 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-28 11:41+0800\n" +"PO-Revision-Date: 2018-10-28 11:45+0806\n" +"Last-Translator: b' '\n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Translated-Using: django-rosetta 0.9.0\n" + +#: registration/serializers.py:67 +msgid "View is not defined, pass it as a context variable" +msgstr "View未定义,请通过context变量传入" + +#: registration/serializers.py:72 +msgid "Define adapter_class in view" +msgstr "请在View中定义adapter_class" + +#: registration/serializers.py:91 +msgid "Define callback_url in view" +msgstr "请在view中定义callback_url" + +#: registration/serializers.py:95 +msgid "Define client_class in view" +msgstr "请在view中定义client_class" + +#: registration/serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "输入错误。access_token或code是必填项。" + +#: registration/serializers.py:125 +msgid "Incorrect value" +msgstr "错误的值" + +#: registration/serializers.py:139 +msgid "User is already registered with this e-mail address." +msgstr "该邮箱地址已被注册。" + +#: registration/serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "该邮箱地址已被注册。" + +#: registration/serializers.py:193 +msgid "The two password fields didn't match." +msgstr "两次输入的密码不相同" + +#: registration/views.py:51 +msgid "Verification e-mail sent." +msgstr "验证邮件已发送。" + +#: registration/views.py:98 +msgid "ok" +msgstr "好的" + +#: serializers.py:33 +msgid "Must include \"email\" and \"password\"." +msgstr "比如包含\"email\"和\"password\"。" + +#: serializers.py:44 +msgid "Must include \"username\" and \"password\"." +msgstr "比如包含\"username\"和\"password\"。" + +#: serializers.py:57 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "比如包含\"username\",\"email\",\"password\"其中一个。" + +#: serializers.py:98 +msgid "User account is disabled." +msgstr "用户账号已被禁用。" + +#: serializers.py:101 +msgid "Unable to log in with provided credentials." +msgstr "无法使用提供的信息登录。" + +#: serializers.py:110 +msgid "E-mail is not verified." +msgstr "邮箱未验证。" + +#: views.py:127 +msgid "Successfully logged out." +msgstr "已成功登出。" + +#: views.py:175 +msgid "Password reset e-mail has been sent." +msgstr "密码重置邮件已发送。" + +#: views.py:201 +msgid "Password has been reset with the new password." +msgstr "密码重置成功。" + +#: views.py:223 +msgid "New password has been saved." +msgstr "新密码已设置成功。" diff --git a/rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo b/rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo new file mode 100644 index 0000000..20e5d8d Binary files /dev/null and b/rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/zh_Hant/LC_MESSAGES/django.po b/rest_auth/locale/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 0000000..b99489a --- /dev/null +++ b/rest_auth/locale/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,103 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-10-28 11:41+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: registration/serializers.py:67 +msgid "View is not defined, pass it as a context variable" +msgstr "" + +#: registration/serializers.py:72 +msgid "Define adapter_class in view" +msgstr "" + +#: registration/serializers.py:91 +msgid "Define callback_url in view" +msgstr "" + +#: registration/serializers.py:95 +msgid "Define client_class in view" +msgstr "" + +#: registration/serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "" + +#: registration/serializers.py:125 +msgid "Incorrect value" +msgstr "" + +#: registration/serializers.py:139 +msgid "User is already registered with this e-mail address." +msgstr "" + +#: registration/serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "" + +#: registration/serializers.py:193 +msgid "The two password fields didn't match." +msgstr "" + +#: registration/views.py:51 +msgid "Verification e-mail sent." +msgstr "" + +#: registration/views.py:98 +msgid "ok" +msgstr "" + +#: serializers.py:33 +msgid "Must include \"email\" and \"password\"." +msgstr "" + +#: serializers.py:44 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: serializers.py:57 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "" + +#: serializers.py:98 +msgid "User account is disabled." +msgstr "" + +#: serializers.py:101 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: serializers.py:110 +msgid "E-mail is not verified." +msgstr "" + +#: views.py:127 +msgid "Successfully logged out." +msgstr "" + +#: views.py:175 +msgid "Password reset e-mail has been sent." +msgstr "" + +#: views.py:201 +msgid "Password has been reset with the new password." +msgstr "" + +#: views.py:223 +msgid "New password has been saved." +msgstr "" diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index c6b5d5b..4f99c18 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,6 +1,6 @@ from django.http import HttpRequest -from django.conf import settings from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth import get_user_model try: from allauth.account import app_settings as allauth_settings @@ -8,15 +8,29 @@ try: get_username_max_length) from allauth.account.adapter import get_adapter from allauth.account.utils import setup_user_email + from allauth.socialaccount.helpers import complete_social_login + from allauth.socialaccount.models import SocialAccount + from allauth.socialaccount.providers.base import AuthProcess except ImportError: raise ImportError("allauth needs to be added to INSTALLED_APPS.") from rest_framework import serializers from requests.exceptions import HTTPError -# Import is needed only if we are using social login, in which -# case the allauth.socialaccount will be declared -if 'allauth.socialaccount' in settings.INSTALLED_APPS: - from allauth.socialaccount.helpers import complete_social_login + + +class SocialAccountSerializer(serializers.ModelSerializer): + """ + serialize allauth SocialAccounts for use with a REST API + """ + class Meta: + model = SocialAccount + fields = ( + 'id', + 'provider', + 'uid', + 'last_login', + 'date_joined', + ) class SocialLoginSerializer(serializers.Serializer): @@ -108,16 +122,47 @@ class SocialLoginSerializer(serializers.Serializer): login = self.get_social_login(adapter, app, social_token, access_token) complete_social_login(request, login) except HTTPError: - raise serializers.ValidationError(_('Incorrect value')) + raise serializers.ValidationError(_("Incorrect value")) if not login.is_existing: + # We have an account already signed up in a different flow + # with the same email address: raise an exception. + # This needs to be handled in the frontend. We can not just + # link up the accounts due to security constraints + if allauth_settings.UNIQUE_EMAIL: + # Do we have an account already with this email address? + account_exists = get_user_model().objects.filter( + email=login.user.email, + ).exists() + if account_exists: + raise serializers.ValidationError( + _("User is already registered with this e-mail address.") + ) + login.lookup() login.save(request, connect=True) + attrs['user'] = login.account.user return attrs +class SocialConnectMixin(object): + def get_social_login(self, *args, **kwargs): + """ + Set the social login process state to connect rather than login + Refer to the implementation of get_social_login in base class and to the + allauth.socialaccount.helpers module complete_social_login function. + """ + social_login = super(SocialConnectMixin, self).get_social_login(*args, **kwargs) + social_login.state['process'] = AuthProcess.CONNECT + return social_login + + +class SocialConnectSerializer(SocialConnectMixin, SocialLoginSerializer): + pass + + class RegisterSerializer(serializers.Serializer): username = serializers.CharField( max_length=get_username_max_length(), diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index fd80e82..8cb4d3c 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -5,22 +5,29 @@ from django.views.decorators.debug import sensitive_post_parameters from rest_framework.views import APIView from rest_framework.response import Response -from rest_framework.permissions import AllowAny -from rest_framework.generics import CreateAPIView +from rest_framework.permissions import (AllowAny, + IsAuthenticated) +from rest_framework.generics import CreateAPIView, ListAPIView, GenericAPIView +from rest_framework.exceptions import NotFound from rest_framework import status from allauth.account.adapter import get_adapter from allauth.account.views import ConfirmEmailView from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings +from allauth.socialaccount import signals +from allauth.socialaccount.adapter import get_adapter as get_social_adapter +from allauth.socialaccount.models import SocialAccount from rest_auth.app_settings import (UserDetailsSerializer, TokenSerializer, JWTSerializer, create_token) from rest_auth.models import TokenModel -from rest_auth.registration.serializers import (SocialLoginSerializer, - VerifyEmailSerializer) +from rest_auth.registration.serializers import (VerifyEmailSerializer, + SocialLoginSerializer, + SocialAccountSerializer, + SocialConnectSerializer) from rest_auth.utils import jwt_encode from rest_auth.views import LoginView from .app_settings import RegisterSerializer, register_permission_classes @@ -113,12 +120,70 @@ class SocialLoginView(LoginView): class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter - client_class = OAuth2Client - callback_url = 'localhost:8000' + client_class = OAuth2Client + callback_url = 'localhost:8000' ------------- """ - serializer_class = SocialLoginSerializer def process_login(self): get_adapter(self.request).login(self.request, self.user) + + +class SocialConnectView(LoginView): + """ + class used for social account linking + + example usage for facebook with access_token + ------------- + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + + class FacebookConnect(SocialConnectView): + adapter_class = FacebookOAuth2Adapter + ------------- + """ + serializer_class = SocialConnectSerializer + permission_classes = (IsAuthenticated,) + + def process_login(self): + get_adapter(self.request).login(self.request, self.user) + + +class SocialAccountListView(ListAPIView): + """ + List SocialAccounts for the currently logged in user + """ + serializer_class = SocialAccountSerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return SocialAccount.objects.filter(user=self.request.user) + + +class SocialAccountDisconnectView(GenericAPIView): + """ + Disconnect SocialAccount from remote service for + the currently logged in user + """ + serializer_class = SocialConnectSerializer + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return SocialAccount.objects.filter(user=self.request.user) + + def post(self, request, *args, **kwargs): + accounts = self.get_queryset() + account = accounts.filter(pk=kwargs['pk']).first() + if not account: + raise NotFound + + get_social_adapter(self.request).validate_disconnect(account, accounts) + + account.delete() + signals.social_account_removed.send( + sender=SocialAccount, + request=self.request, + socialaccount=account + ) + + return Response(self.get_serializer(account).data) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 723cfca..b645231 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -21,11 +21,14 @@ class LoginSerializer(serializers.Serializer): email = serializers.EmailField(required=False, allow_blank=True) password = serializers.CharField(style={'input_type': 'password'}) + def authenticate(self, **kwargs): + return authenticate(self.context['request'], **kwargs) + def _validate_email(self, email, password): user = None if email and password: - user = authenticate(email=email, password=password) + user = self.authenticate(email=email, password=password) else: msg = _('Must include "email" and "password".') raise exceptions.ValidationError(msg) @@ -36,7 +39,7 @@ class LoginSerializer(serializers.Serializer): user = None if username and password: - user = authenticate(username=username, password=password) + user = self.authenticate(username=username, password=password) else: msg = _('Must include "username" and "password".') raise exceptions.ValidationError(msg) @@ -47,9 +50,9 @@ class LoginSerializer(serializers.Serializer): user = None if email and password: - user = authenticate(email=email, password=password) + user = self.authenticate(email=email, password=password) elif username and password: - user = authenticate(username=username, password=password) + user = self.authenticate(username=username, password=password) else: msg = _('Must include either "username" or "email" and "password".') raise exceptions.ValidationError(msg) @@ -71,7 +74,7 @@ class LoginSerializer(serializers.Serializer): user = self._validate_email(email, password) # Authentication through username - if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: + elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: user = self._validate_username(username, password) # Authentication through either username or email @@ -146,7 +149,7 @@ class JWTSerializer(serializers.Serializer): JWTUserDetailsSerializer = import_callable( rest_auth_serializers.get('USER_DETAILS_SERIALIZER', UserDetailsSerializer) ) - user_data = JWTUserDetailsSerializer(obj['user']).data + user_data = JWTUserDetailsSerializer(obj['user'], context=self.context).data return user_data @@ -220,7 +223,7 @@ class PasswordResetConfirmSerializer(serializers.Serializer): return attrs def save(self): - self.set_password_form.save() + return self.set_password_form.save() class PasswordChangeSerializer(serializers.Serializer): @@ -253,7 +256,8 @@ class PasswordChangeSerializer(serializers.Serializer): ) if all(invalid_password_conditions): - raise serializers.ValidationError('Invalid password') + err_msg = _("Your old password was entered incorrectly. Please enter it again.") + raise serializers.ValidationError(err_msg) return value def validate(self, attrs): diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py index 665b98d..1621813 100644 --- a/rest_auth/social_serializers.py +++ b/rest_auth/social_serializers.py @@ -8,6 +8,8 @@ if 'allauth.socialaccount' in settings.INSTALLED_APPS: from allauth.socialaccount.models import SocialToken from allauth.socialaccount.providers.oauth.client import OAuthError + from rest_auth.registration.serializers import SocialConnectMixin + class TwitterLoginSerializer(serializers.Serializer): access_token = serializers.CharField() @@ -73,3 +75,7 @@ class TwitterLoginSerializer(serializers.Serializer): attrs['user'] = login.account.user return attrs + + +class TwitterConnectSerializer(SocialConnectMixin, TwitterLoginSerializer): + pass diff --git a/rest_auth/tests/test_base.py b/rest_auth/tests/mixins.py similarity index 77% rename from rest_auth/tests/test_base.py rename to rest_auth/tests/mixins.py index 48d94f0..30b3d58 100644 --- a/rest_auth/tests/test_base.py +++ b/rest_auth/tests/mixins.py @@ -1,11 +1,23 @@ import json from django.conf import settings -from django.core.urlresolvers import reverse from django.test.client import Client, MULTIPART_CONTENT from django.utils.encoding import force_text from rest_framework import status +from rest_framework import permissions + +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + + +class CustomPermissionClass(permissions.BasePermission): + message = 'You shall not pass!' + + def has_permission(self, request, view): + return False class APIClient(Client): @@ -17,8 +29,7 @@ class APIClient(Client): return self.generic('OPTIONS', path, data, content_type, **extra) -class BaseAPITestCase(object): - +class TestsMixin(object): """ base for API tests: * easy request calls, f.e.: self.post(url, data), self.get(url) @@ -64,29 +75,6 @@ class BaseAPITestCase(object): def patch(self, *args, **kwargs): return self.send_request('patch', *args, **kwargs) - # def put(self, *args, **kwargs): - # return self.send_request('put', *args, **kwargs) - - # def delete(self, *args, **kwargs): - # return self.send_request('delete', *args, **kwargs) - - # def options(self, *args, **kwargs): - # return self.send_request('options', *args, **kwargs) - - # def post_file(self, *args, **kwargs): - # kwargs['content_type'] = MULTIPART_CONTENT - # return self.send_request('post', *args, **kwargs) - - # def get_file(self, *args, **kwargs): - # content_type = None - # if 'content_type' in kwargs: - # content_type = kwargs.pop('content_type') - # response = self.send_request('get', *args, **kwargs) - # if content_type: - # self.assertEqual( - # bool(filter(lambda x: content_type in x, response._headers['content-type'])), True) - # return response - def init(self): settings.DEBUG = True self.client = APIClient() @@ -102,6 +90,9 @@ class BaseAPITestCase(object): self.tw_login_url = reverse('tw_login') self.tw_login_no_view_url = reverse('tw_login_no_view') self.tw_login_no_adapter_url = reverse('tw_login_no_adapter') + self.fb_connect_url = reverse('fb_connect') + self.tw_connect_url = reverse('tw_connect') + self.social_account_list_url = reverse('social_account_list') def _login(self): payload = { diff --git a/rest_auth/tests/requirements.pip b/rest_auth/tests/requirements.pip index 5a30385..f48ee3c 100644 --- a/rest_auth/tests/requirements.pip +++ b/rest_auth/tests/requirements.pip @@ -2,3 +2,4 @@ django-allauth>=0.25.0 responses>=0.3.0 flake8==2.4.0 djangorestframework-jwt>=1.7.2 +djangorestframework>=3.6.4 diff --git a/rest_auth/tests/settings.py b/rest_auth/tests/settings.py index 060cc89..e353fb0 100644 --- a/rest_auth/tests/settings.py +++ b/rest_auth/tests/settings.py @@ -25,7 +25,7 @@ DATABASES = { } } -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -33,6 +33,9 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.messages.middleware.MessageMiddleware' ] +# Adding for backwards compatibility for Django 1.8 tests +MIDDLEWARE_CLASSES = MIDDLEWARE + TEMPLATE_CONTEXT_PROCESSORS = [ 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index c2cc4f3..bdacf46 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -1,17 +1,26 @@ -from django.core.urlresolvers import reverse from django.test import TestCase, override_settings from django.contrib.auth import get_user_model from django.core import mail from django.conf import settings from django.utils.encoding import force_text -from rest_framework import status from allauth.account import app_settings as account_app_settings -from .test_base import BaseAPITestCase +from rest_framework import status +from rest_framework.test import APIRequestFactory + +from rest_auth.registration.views import RegisterView +from rest_auth.registration.app_settings import register_permission_classes + +from .mixins import TestsMixin, CustomPermissionClass + +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse @override_settings(ROOT_URLCONF="tests.urls") -class APITestCase1(TestCase, BaseAPITestCase): +class APIBasicTests(TestsMixin, TestCase): """ Case #1: - user profile: defined @@ -413,6 +422,20 @@ class APITestCase1(TestCase, BaseAPITestCase): self._login() self._logout() + @override_settings(REST_AUTH_REGISTER_PERMISSION_CLASSES=(CustomPermissionClass,)) + def test_registration_with_custom_permission_class(self): + + class CustomRegisterView(RegisterView): + permission_classes = register_permission_classes() + authentication_classes = () + + factory = APIRequestFactory() + request = factory.post('/customer/details', self.REGISTRATION_DATA, format='json') + + response = CustomRegisterView.as_view()(request) + self.assertEqual(response.data['detail'], CustomPermissionClass.message) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + @override_settings(REST_USE_JWT=True) def test_registration_with_jwt(self): user_count = get_user_model().objects.all().count() diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py index 47ac0bb..830e631 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -5,17 +5,22 @@ from django.contrib.auth import get_user_model from django.test.utils import override_settings from django.contrib.sites.models import Site +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + from allauth.socialaccount.models import SocialApp from allauth.socialaccount.providers.facebook.provider import GRAPH_API_URL import responses from rest_framework import status -from .test_base import BaseAPITestCase +from .mixins import TestsMixin @override_settings(ROOT_URLCONF="tests.urls") -class TestSocialAuth(TestCase, BaseAPITestCase): +class TestSocialAuth(TestsMixin, TestCase): USERNAME = 'person' PASS = 'person' @@ -225,7 +230,7 @@ class TestSocialAuth(TestCase, BaseAPITestCase): REST_SESSION_LOGIN=False, ACCOUNT_EMAIL_CONFIRMATION_HMAC=False ) - def test_edge_case(self): + def test_email_clash_with_existing_account(self): resp_body = { "id": "123123123123", "first_name": "John", @@ -251,6 +256,8 @@ class TestSocialAuth(TestCase, BaseAPITestCase): # test empty payload self.post(self.register_url, data={}, status_code=400) + + # register user and send email confirmation self.post( self.register_url, data=self.REGISTRATION_DATA, @@ -271,12 +278,11 @@ class TestSocialAuth(TestCase, BaseAPITestCase): self._login() self._logout() + # fb log in with already existing email payload = { 'access_token': 'abc123' } - - self.post(self.fb_login_url, data=payload, status_code=200) - self.assertIn('key', self.response.json.keys()) + self.post(self.fb_login_url, data=payload, status_code=400) @responses.activate @override_settings( @@ -302,3 +308,140 @@ class TestSocialAuth(TestCase, BaseAPITestCase): self.assertIn('user', self.response.json.keys()) self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + + +@override_settings(ROOT_URLCONF="tests.urls") +class TestSocialConnectAuth(TestsMixin, TestCase): + + USERNAME = 'person' + PASS = 'person' + EMAIL = "person1@world.com" + REGISTRATION_DATA = { + "username": USERNAME, + "password1": PASS, + "password2": PASS, + "email": EMAIL + } + + def setUp(self): + self.init() + + facebook_social_app = SocialApp.objects.create( + provider='facebook', + name='Facebook', + client_id='123123123', + secret='321321321', + ) + + twitter_social_app = SocialApp.objects.create( + provider='twitter', + name='Twitter', + client_id='11223344', + secret='55667788', + ) + + site = Site.objects.get_current() + facebook_social_app.sites.add(site) + twitter_social_app.sites.add(site) + self.graph_api_url = GRAPH_API_URL + '/me' + self.twitter_url = 'https://api.twitter.com/1.1/account/verify_credentials.json' + + @responses.activate + def test_social_connect_no_auth(self): + responses.add( + responses.GET, + self.graph_api_url, + body='', + status=200, + content_type='application/json' + ) + + payload = { + 'access_token': 'abc123' + } + self.post(self.fb_connect_url, data=payload, status_code=403) + self.post(self.tw_connect_url, data=payload, status_code=403) + + @responses.activate + def test_social_connect(self): + # register user + self.post( + self.register_url, + data=self.REGISTRATION_DATA, + status_code=201 + ) + + # Test Facebook + resp_body = { + "id": "123123123123", + "first_name": "John", + "gender": "male", + "last_name": "Smith", + "link": "https://www.facebook.com/john.smith", + "locale": "en_US", + "name": "John Smith", + "timezone": 2, + "updated_time": "2014-08-13T10:14:38+0000", + "username": "john.smith", + "verified": True + } + + responses.add( + responses.GET, + self.graph_api_url, + body=json.dumps(resp_body), + status=200, + content_type='application/json' + ) + + payload = { + 'access_token': 'abc123' + } + self.post(self.fb_connect_url, data=payload, status_code=200) + self.assertIn('key', self.response.json.keys()) + + # Test Twitter + resp_body = { + "id": "123123123123", + } + + responses.add( + responses.GET, + self.twitter_url, + body=json.dumps(resp_body), + status=200, + content_type='application/json' + ) + + payload = { + 'access_token': 'abc123', + 'token_secret': '1111222233334444' + } + + self.post(self.tw_connect_url, data=payload) + + self.assertIn('key', self.response.json.keys()) + + # Check current social accounts + self.get(self.social_account_list_url) + self.assertEqual(len(self.response.json), 2) + self.assertEqual(self.response.json[0]['provider'], 'facebook') + self.assertEqual(self.response.json[1]['provider'], 'twitter') + + facebook_social_account_id = self.response.json[0]['id'] + + # Try disconnecting accounts + self.incorrect_disconnect_url = reverse( + 'social_account_disconnect', args=[999999999] + ) + self.post(self.incorrect_disconnect_url, status_code=404) + + self.disconnect_url = reverse( + 'social_account_disconnect', args=[facebook_social_account_id] + ) + self.post(self.disconnect_url, status_code=200) + + # Check social accounts after disconnecting + self.get(self.social_account_list_url) + self.assertEqual(len(self.response.json), 1) + self.assertEqual(self.response.json[0]['provider'], 'twitter') diff --git a/rest_auth/tests/urls.py b/rest_auth/tests/urls.py index 6371218..401f23a 100644 --- a/rest_auth/tests/urls.py +++ b/rest_auth/tests/urls.py @@ -8,8 +8,13 @@ from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter from rest_framework.decorators import api_view from rest_auth.urls import urlpatterns -from rest_auth.registration.views import SocialLoginView -from rest_auth.social_serializers import TwitterLoginSerializer +from rest_auth.registration.views import ( + SocialLoginView, SocialConnectView, SocialAccountListView, + SocialAccountDisconnectView +) +from rest_auth.social_serializers import ( + TwitterLoginSerializer, TwitterConnectSerializer +) class FacebookLogin(SocialLoginView): @@ -21,6 +26,15 @@ class TwitterLogin(SocialLoginView): serializer_class = TwitterLoginSerializer +class FacebookConnect(SocialConnectView): + adapter_class = FacebookOAuth2Adapter + + +class TwitterConnect(SocialConnectView): + adapter_class = TwitterOAuthAdapter + serializer_class = TwitterConnectSerializer + + class TwitterLoginSerializerFoo(TwitterLoginSerializer): pass @@ -49,5 +63,10 @@ urlpatterns += [ url(r'^social-login/twitter/$', TwitterLogin.as_view(), name='tw_login'), url(r'^social-login/twitter-no-view/$', twitter_login_view, name='tw_login_no_view'), url(r'^social-login/twitter-no-adapter/$', TwitterLoginNoAdapter.as_view(), name='tw_login_no_adapter'), + url(r'^social-login/facebook/connect/$', FacebookConnect.as_view(), name='fb_connect'), + url(r'^social-login/twitter/connect/$', TwitterConnect.as_view(), name='tw_connect'), + url(r'^socialaccounts/$', SocialAccountListView.as_view(), name='social_account_list'), + url(r'^socialaccounts/(?P\d+)/disconnect/$', SocialAccountDisconnectView.as_view(), + name='social_account_disconnect'), url(r'^accounts/', include('allauth.socialaccount.urls')) ] diff --git a/rest_auth/views.py b/rest_auth/views.py index 65c6726..5d488c8 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -89,11 +89,22 @@ class LoginView(GenericAPIView): serializer = serializer_class(instance=self.user, context={'request': self.request}) - return Response(serializer.data, status=status.HTTP_200_OK) + response = Response(serializer.data, status=status.HTTP_200_OK) + if getattr(settings, 'REST_USE_JWT', False): + from rest_framework_jwt.settings import api_settings as jwt_settings + if jwt_settings.JWT_AUTH_COOKIE: + from datetime import datetime + expiration = (datetime.utcnow() + jwt_settings.JWT_EXPIRATION_DELTA) + response.set_cookie(jwt_settings.JWT_AUTH_COOKIE, + self.token, + expires=expiration, + httponly=True) + return response def post(self, request, *args, **kwargs): self.request = request - self.serializer = self.get_serializer(data=self.request.data) + self.serializer = self.get_serializer(data=self.request.data, + context={'request': request}) self.serializer.is_valid(raise_exception=True) self.login() @@ -117,7 +128,7 @@ class LogoutView(APIView): return self.finalize_response(request, response, *args, **kwargs) - def post(self, request): + def post(self, request, *args, **kwargs): return self.logout(request) def logout(self, request): @@ -125,11 +136,16 @@ class LogoutView(APIView): request.user.auth_token.delete() except (AttributeError, ObjectDoesNotExist): pass + if getattr(settings, 'REST_SESSION_LOGIN', True): + django_logout(request) - django_logout(request) - - return Response({"detail": _("Successfully logged out.")}, - status=status.HTTP_200_OK) + response = Response({"detail": _("Successfully logged out.")}, + status=status.HTTP_200_OK) + if getattr(settings, 'REST_USE_JWT', False): + from rest_framework_jwt.settings import api_settings as jwt_settings + if jwt_settings.JWT_AUTH_COOKIE: + response.delete_cookie(jwt_settings.JWT_AUTH_COOKIE) + return response class UserDetailsView(RetrieveUpdateAPIView): @@ -197,7 +213,7 @@ class PasswordResetConfirmView(GenericAPIView): def dispatch(self, *args, **kwargs): return super(PasswordResetConfirmView, self).dispatch(*args, **kwargs) - def post(self, request): + def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() @@ -220,7 +236,7 @@ class PasswordChangeView(GenericAPIView): def dispatch(self, *args, **kwargs): return super(PasswordChangeView, self).dispatch(*args, **kwargs) - def post(self, request): + def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() diff --git a/.coveragerc b/setup.cfg similarity index 70% rename from .coveragerc rename to setup.cfg index 70d6d0d..8d915a7 100644 --- a/.coveragerc +++ b/setup.cfg @@ -1,8 +1,21 @@ -# .coveragerc to control coverage.py -[run] +[bdist_wheel] +universal = 1 + + +[metadata] +license_file = LICENSE + + +[flake8] +max-line-length = 120 +exclude = docs/*,demo/* +ignore = F403 + + +[coverage:run] omit=*site-packages*,*distutils*,*migrations* -[report] +[coverage:report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma @@ -22,5 +35,5 @@ exclude_lines = ignore_errors = True -[html] -directory = coverage_html \ No newline at end of file +[coverage:html] +directory = coverage_html diff --git a/setup.py b/setup.py index 84ed034..675b99d 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,8 @@ #!/usr/bin/env python -try: - from setuptools import setup, find_packages -except ImportError: - from ez_setup import use_setuptools - use_setuptools() - from setuptools import setup, find_packages - - import os +from setuptools import setup, find_packages + here = os.path.dirname(os.path.abspath(__file__)) f = open(os.path.join(here, 'README.rst')) @@ -18,7 +12,7 @@ f.close() setup( name='django-rest-auth', - version='0.9.1', + version='0.9.5', author='Sumit Chachra', author_email='chachra@tivix.com', url='http://github.com/Tivix/django-rest-auth', @@ -29,7 +23,7 @@ setup( zip_safe=False, install_requires=[ 'Django>=1.8.0', - 'djangorestframework>=3.1.0', + 'djangorestframework>=3.1.3', 'six>=1.9.0', ], extras_require={