diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..efcf792 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,34 @@ +version: 2.1 +orbs: + docker: circleci/docker@0.6.0 + +jobs: + test-django-3: &template + docker: + - image: circleci/python:3.8.0 + environment: + DJANGO_VERSION: 3.0.3 + DRF: 3.11 + executor: docker/docker + steps: + - checkout + - run: pip install --user -r dev-requirements.txt + - run: pip install --user -r dj_rest_auth/tests/requirements.pip + - run: pip install -q --user coveralls djangorestframework==$DRF Django==$DJANGO_VERSION + - run: + command: coverage run --source=dj_rest_auth setup.py test + name: Test + - run: + command: COVERALLS_REPO_TOKEN=Q58WdUuZOi89XHyDeDsGE2lxUGQ2IfqP3 coveralls + name: Coverage + test-django-2: + <<: *template + environment: + DJANGO_VERSION: 2.2.10 + DRF: 3.9 + +workflows: + main: + jobs: + - test-django-3 + - test-django-2 \ No newline at end of file diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..16eb397 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-pro +repo_token: Q58WdUuZOi89XHyDeDsGE2lxUGQ2IfqP3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 894a44c..136132c 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,9 @@ target/ # Jupyter Notebook .ipynb_checkpoints +# IDE +.idea + # pyenv .python-version @@ -102,3 +105,5 @@ venv.bak/ # mypy .mypy_cache/ +demo/react-spa/node_modules/ +demo/react-spa/yarn.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b059c1e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python -python: - - "2.7" - - "3.5" - - "3.6" -env: - - 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 djangorestframework==$DRF - - pip install coveralls - - pip install -r rest_auth/tests/requirements.pip -script: - - coverage run --source=rest_auth setup.py test -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/AUTHORS b/AUTHORS index 21906da..e41efa7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1 @@ -http://github.com/Tivix/django-rest-auth/contributors +http://github.com/jazzband/dj-rest-auth/contributors diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..10d7919 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) + +This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1e3b853..42a0292 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Tivix +Copyright (c) 2014 iMerica https://github.com/iMerica/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 01a589f..c2d4208 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,4 @@ include AUTHORS include LICENSE include MANIFEST.in include README.md -graft rest_auth +graft dj_rest_auth diff --git a/README.md b/README.md new file mode 100644 index 0000000..06d9953 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Dj-Rest-Auth +[![](https://circleci.com/gh/jazzband/dj-rest-auth.svg?style=svg)](https://app.circleci.com/pipelines/github/jazzband/dj-rest-auth) +[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) +[![Coverage Status](https://coveralls.io/repos/github/jazzband/dj-rest-auth/badge.svg?branch=master)](https://coveralls.io/github/jazzband/dj-rest-auth?branch=master) + +Drop-in API endpoints for handling authentication securely in Django Rest Framework. Works especially well +with SPAs (e.g React, Vue, Angular), and Mobile applications. + +## Requirements +- Django 2 or 3. +- Python 3 + +## Quick Setup + +Install package + + pip install dj-rest-auth + +Add `dj_rest_auth` app to INSTALLED_APPS in your django settings.py: + +```python +INSTALLED_APPS = ( + ..., + 'rest_framework', + 'rest_framework.authtoken', + ..., + 'dj_rest_auth' +) +``` + +Add URL patterns + +```python +urlpatterns = [ + url(r'^dj-rest-auth/', include('dj_rest_auth.urls')) +] +``` + + +(Optional) Use Http-Only cookies + +```python +REST_USE_JWT = True +JWT_AUTH_COOKIE = 'jwt-auth' +``` + +### Testing + +To run the tests within a virtualenv, run `python runtests.py` from the repository directory. +The easiest way to run test coverage is with [`coverage`](https://pypi.org/project/coverage/), +which runs the tests against all supported Django installs. To run the test coverage +within a virtualenv, run `coverage run ./runtests.py` from the repository directory then run `coverage report`. + +#### Tox + +Testing may also be done using [`tox`](https://pypi.org/project/tox/), which +will run the tests against all supported combinations of python and django. + +Install tox, either globally or within a virtualenv, and then simply run `tox` +from the repository directory. As there are many combinations, you may run them +in [`parallel`](https://tox.readthedocs.io/en/latest/config.html#cmdoption-tox-p) +using `tox --parallel`. + +The `tox.ini` includes an environment for testing code [`coverage`](https://pypi.org/project/coverage/) +and you can run it and view this report with `tox -e coverage`. + +Linting may also be performed via [`flake8`](https://pypi.org/project/flake8/) +by running `tox -e flake8`. + +### Documentation + +View the full documentation here: https://dj-rest-auth.readthedocs.io/en/latest/index.html + + +### Acknowledgements + +This project began as a fork of `django-rest-auth`. Big thanks to everyone who contributed to that repo! diff --git a/README.rst b/README.rst deleted file mode 100644 index ac7f19e..0000000 --- a/README.rst +++ /dev/null @@ -1,31 +0,0 @@ -Welcome to django-rest-auth -=========================== - -.. 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.svg - :target: https://coveralls.io/r/Tivix/django-rest-auth?branch=master - - -.. image:: https://readthedocs.org/projects/django-rest-auth/badge/?version=latest - :target: https://readthedocs.org/projects/django-rest-auth/?badge=latest - - -Django-rest-auth provides a set of REST API endpoints for Authentication and Registration - - -Documentation -------------- -http://django-rest-auth.readthedocs.org/en/latest/ - - -Source code ------------ -https://github.com/Tivix/django-rest-auth - - -Stack Overflow ------------ -http://stackoverflow.com/questions/tagged/django-rest-auth diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 514a8fb..4496f0d 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -31,23 +31,23 @@ INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', - # 'django.contrib.messages', + 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', - 'rest_framework', 'rest_framework.authtoken', - 'rest_auth', - + 'dj_rest_auth', 'allauth', 'allauth.account', - 'rest_auth.registration', + 'dj_rest_auth.registration', 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', 'rest_framework_swagger', + 'corsheaders' ) MIDDLEWARE = ( + 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -116,14 +116,23 @@ ACCOUNT_EMAIL_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = 'username' ACCOUNT_EMAIL_VERIFICATION = 'optional' +REST_USE_JWT = True +JWT_AUTH_COOKIE = 'auth' + REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', - ) + 'dj_rest_auth.utils.JWTCookieAuthentication' + ), + 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } SWAGGER_SETTINGS = { 'LOGIN_URL': 'login', 'LOGOUT_URL': 'logout', } + + +# For demo purposes only. Use a white list in the real world. +CORS_ORIGIN_ALLOW_ALL = True diff --git a/demo/demo/urls.py b/demo/demo/urls.py index af7d38b..4f63ba1 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -1,7 +1,6 @@ from django.conf.urls import include, url from django.contrib import admin -from django.views.generic import TemplateView, RedirectView - +from django.views.generic import RedirectView, TemplateView from rest_framework_swagger.views import get_swagger_view urlpatterns = [ @@ -35,8 +34,8 @@ urlpatterns = [ TemplateView.as_view(template_name="password_reset_confirm.html"), name='password_reset_confirm'), - url(r'^rest-auth/', include('rest_auth.urls')), - url(r'^rest-auth/registration/', include('rest_auth.registration.urls')), + url(r'^dj-rest-auth/', include('dj_rest_auth.urls')), + url(r'^dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')), url(r'^account/', include('allauth.urls')), url(r'^admin/', admin.site.urls), url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), diff --git a/demo/demo/wsgi.py b/demo/demo/wsgi.py index ef48754..863501d 100644 --- a/demo/demo/wsgi.py +++ b/demo/demo/wsgi.py @@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") + application = get_wsgi_application() diff --git a/demo/react-spa/.gitignore b/demo/react-spa/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/demo/react-spa/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/demo/react-spa/README.md b/demo/react-spa/README.md new file mode 100644 index 0000000..9c40dcd --- /dev/null +++ b/demo/react-spa/README.md @@ -0,0 +1,68 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.
+You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.
+It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting + +### Analyzing the Bundle Size + +This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size + +### Making a Progressive Web App + +This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app + +### Advanced Configuration + +This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration + +### Deployment + +This section has moved here: https://facebook.github.io/create-react-app/docs/deployment + +### `yarn build` fails to minify + +This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify diff --git a/demo/react-spa/package.json b/demo/react-spa/package.json new file mode 100644 index 0000000..62f4226 --- /dev/null +++ b/demo/react-spa/package.json @@ -0,0 +1,34 @@ +{ + "name": "react-spa", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^7.1.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-scripts": "3.4.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/demo/react-spa/public/favicon.ico b/demo/react-spa/public/favicon.ico new file mode 100644 index 0000000..bcd5dfd Binary files /dev/null and b/demo/react-spa/public/favicon.ico differ diff --git a/demo/react-spa/public/index.html b/demo/react-spa/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/demo/react-spa/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/demo/react-spa/public/logo192.png b/demo/react-spa/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/demo/react-spa/public/logo192.png differ diff --git a/demo/react-spa/public/logo512.png b/demo/react-spa/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/demo/react-spa/public/logo512.png differ diff --git a/demo/react-spa/public/manifest.json b/demo/react-spa/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/demo/react-spa/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/demo/react-spa/public/robots.txt b/demo/react-spa/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/demo/react-spa/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/demo/react-spa/src/App.css b/demo/react-spa/src/App.css new file mode 100644 index 0000000..92fd3ac --- /dev/null +++ b/demo/react-spa/src/App.css @@ -0,0 +1,40 @@ +.App { + text-align: center; + width: 100%; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; + width: 100%; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/demo/react-spa/src/App.js b/demo/react-spa/src/App.js new file mode 100644 index 0000000..a8546a4 --- /dev/null +++ b/demo/react-spa/src/App.js @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import './App.css'; + +function App() { + const [ resp, changeResponse ] = useState(null); + const [ username, changeUsername ] = useState(''); + const [ password, changePassword ] = useState(''); + + function onSubmit(e) { + e.preventDefault(); + return fetch('http://localhost:8000/dj-rest-auth/login/', { + method: 'POST', + credentials: 'omit', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({username, password}) + }).then(resp => resp.json()).then(data => { + changeResponse(data) + }).catch(error => console.log('error ->', error)) + } + + return ( +
+
+

+ Login +

+
+ Inspect the network requests in your browser to view headers returned by dj-rest-auth. +
+
+ {resp && +
+ + {JSON.stringify(resp)} + +
+ } +
+
+
+
+ changeUsername(e.target.value)} + value={username} + type={'input'} + name={'username'}/> +
+
+ changePassword(e.target.value)} + value={password} + type={'password'} + name={'password'}/> +
+ +
+
+
+
+ ); +} + +export default App; diff --git a/demo/react-spa/src/App.test.js b/demo/react-spa/src/App.test.js new file mode 100644 index 0000000..4db7ebc --- /dev/null +++ b/demo/react-spa/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + const { getByText } = render(); + const linkElement = getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/demo/react-spa/src/index.css b/demo/react-spa/src/index.css new file mode 100644 index 0000000..379ece5 --- /dev/null +++ b/demo/react-spa/src/index.css @@ -0,0 +1,48 @@ +body { + margin: 0; + width: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: flex; + flex-direction: column; +} + +body div { + flex-direction: row; + text-align: center; + display: flex; + justify-content: center; +} + +ul { + width: 240px; + font-size: 11px; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; + word-break: break-all; + height: 200px; + color: #0f0f0f; +} + +.response { + margin: 20px; + background-color: #eee; + width: 80%; + height: 200px; + overflow: scroll; +} + +.help-text { + font-size: 11px; + margin: 20px; +} + +form > div { + margin: 20px; +} diff --git a/demo/react-spa/src/index.js b/demo/react-spa/src/index.js new file mode 100644 index 0000000..f5185c1 --- /dev/null +++ b/demo/react-spa/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import * as serviceWorker from './serviceWorker'; + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/demo/react-spa/src/logo.svg b/demo/react-spa/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/demo/react-spa/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/demo/react-spa/src/serviceWorker.js b/demo/react-spa/src/serviceWorker.js new file mode 100644 index 0000000..b04b771 --- /dev/null +++ b/demo/react-spa/src/serviceWorker.js @@ -0,0 +1,141 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' }, + }) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then(registration => { + registration.unregister(); + }) + .catch(error => { + console.error(error.message); + }); + } +} diff --git a/demo/react-spa/src/setupTests.js b/demo/react-spa/src/setupTests.js new file mode 100644 index 0000000..74b1a27 --- /dev/null +++ b/demo/react-spa/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom/extend-expect'; diff --git a/demo/requirements.pip b/demo/requirements.pip index 40b4820..e9276aa 100644 --- a/demo/requirements.pip +++ b/demo/requirements.pip @@ -1,6 +1,8 @@ django>=1.9.0 -django-rest-auth==0.9.5 -djangorestframework>=3.7.0 +git+https://github.com/jazzband/dj-rest-auth.git@master +djangorestframework>=3.11.0 +djangorestframework-simplejwt==4.4.0 django-allauth>=0.24.1 -six==1.9.0 django-rest-swagger==2.0.7 +django-cors-headers==3.2.1 +coreapi==2.3.3 diff --git a/demo/templates/base.html b/demo/templates/base.html index cbf02b6..d57ece3 100644 --- a/demo/templates/base.html +++ b/demo/templates/base.html @@ -4,10 +4,10 @@ - - + + - django-rest-auth demo + dj-rest-auth demo @@ -53,13 +53,13 @@ - django-rest-auth demo + dj-rest-auth demo diff --git a/demo/templates/home.html b/demo/templates/home.html index 134198a..ecc6f7c 100644 --- a/demo/templates/home.html +++ b/demo/templates/home.html @@ -3,7 +3,7 @@ {% block content %}
-

django-rest-auth demo

-

Welcome in django-rest-auth demo project!

+

dj-rest-auth demo

+

Welcome in dj-rest-auth demo project!

{% endblock %} diff --git a/dev-requirements.txt b/dev-requirements.txt index 0a2a9e4..cee8ea6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ --editable . responses>=0.5.0 -djangorestframework-jwt +djangorestframework-simplejwt==4.4.0 django-allauth +coveralls>=1.11.1 \ No newline at end of file diff --git a/rest_auth/__init__.py b/dj_rest_auth/__init__.py similarity index 100% rename from rest_auth/__init__.py rename to dj_rest_auth/__init__.py diff --git a/dj_rest_auth/__version__.py b/dj_rest_auth/__version__.py new file mode 100644 index 0000000..c61dbeb --- /dev/null +++ b/dj_rest_auth/__version__.py @@ -0,0 +1,8 @@ +__title__ = 'dj-rest-auth' +__description__ = 'Authentication and Registration in Django Rest Framework.' +__url__ = 'http://github.com/jazzband/dj-rest-auth' +__version__ = '1.0.6' +__author__ = '@iMerica https://github.com/iMerica' +__author_email__ = 'imichael@pm.me' +__license__ = 'MIT' +__copyright__ = 'Copyright 2020 @iMerica https://github.com/iMerica' diff --git a/rest_auth/admin.py b/dj_rest_auth/admin.py similarity index 100% rename from rest_auth/admin.py rename to dj_rest_auth/admin.py diff --git a/dj_rest_auth/app_settings.py b/dj_rest_auth/app_settings.py new file mode 100644 index 0000000..fe6e6a2 --- /dev/null +++ b/dj_rest_auth/app_settings.py @@ -0,0 +1,40 @@ +from dj_rest_auth.serializers import JWTSerializer as DefaultJWTSerializer +from dj_rest_auth.serializers import LoginSerializer as DefaultLoginSerializer +from dj_rest_auth.serializers import \ + PasswordChangeSerializer as DefaultPasswordChangeSerializer +from dj_rest_auth.serializers import \ + PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer +from dj_rest_auth.serializers import \ + PasswordResetSerializer as DefaultPasswordResetSerializer +from dj_rest_auth.serializers import TokenSerializer as DefaultTokenSerializer +from dj_rest_auth.serializers import \ + UserDetailsSerializer as DefaultUserDetailsSerializer +from django.conf import settings + +from .utils import default_create_token, import_callable + +create_token = import_callable(getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token)) + +serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) + +TokenSerializer = import_callable(serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) + +JWTSerializer = import_callable(serializers.get('JWT_SERIALIZER', DefaultJWTSerializer)) + +UserDetailsSerializer = import_callable(serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer)) + +LoginSerializer = import_callable(serializers.get('LOGIN_SERIALIZER', DefaultLoginSerializer)) + +PasswordResetSerializer = import_callable(serializers.get( + 'PASSWORD_RESET_SERIALIZER', DefaultPasswordResetSerializer +)) + +PasswordResetConfirmSerializer = import_callable(serializers.get( + 'PASSWORD_RESET_CONFIRM_SERIALIZER', DefaultPasswordResetConfirmSerializer +)) + +PasswordChangeSerializer = import_callable( + serializers.get('PASSWORD_CHANGE_SERIALIZER', DefaultPasswordChangeSerializer) +) + +JWT_AUTH_COOKIE = getattr(settings, 'JWT_AUTH_COOKIE', None) diff --git a/rest_auth/locale/cs/LC_MESSAGES/django.mo b/dj_rest_auth/locale/cs/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/cs/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/cs/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/cs/LC_MESSAGES/django.po b/dj_rest_auth/locale/cs/LC_MESSAGES/django.po similarity index 91% rename from rest_auth/locale/cs/LC_MESSAGES/django.po rename to dj_rest_auth/locale/cs/LC_MESSAGES/django.po index eb8c60c..90e6f71 100644 --- a/rest_auth/locale/cs/LC_MESSAGES/django.po +++ b/dj_rest_auth/locale/cs/LC_MESSAGES/django.po @@ -1,11 +1,11 @@ -# Czech translations of Tivix/django-rest-auth +# Czech translations of jazzband/dj-rest-auth # -# This file is distributed under the same license as the Tivix/django-rest-auth package. +# This file is distributed under the same license as the jazzband/dj-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" +"Project-Id-Version: jazzband/dj-rest-auth\n" +"Report-Msgid-Bugs-To: https://github.com/jazzband/dj-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" diff --git a/rest_auth/locale/de/LC_MESSAGES/django.mo b/dj_rest_auth/locale/de/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/de/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/de/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/de/LC_MESSAGES/django.po b/dj_rest_auth/locale/de/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/de/LC_MESSAGES/django.po rename to dj_rest_auth/locale/de/LC_MESSAGES/django.po diff --git a/rest_auth/locale/es/LC_MESSAGES/django.mo b/dj_rest_auth/locale/es/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/es/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/es/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/es/LC_MESSAGES/django.po b/dj_rest_auth/locale/es/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/es/LC_MESSAGES/django.po rename to dj_rest_auth/locale/es/LC_MESSAGES/django.po diff --git a/rest_auth/locale/fr/LC_MESSAGES/django.mo b/dj_rest_auth/locale/fr/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/fr/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/fr/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/fr/LC_MESSAGES/django.po b/dj_rest_auth/locale/fr/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/fr/LC_MESSAGES/django.po rename to dj_rest_auth/locale/fr/LC_MESSAGES/django.po diff --git a/dj_rest_auth/locale/it/LC_MESSAGES/django.mo b/dj_rest_auth/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..e687486 Binary files /dev/null and b/dj_rest_auth/locale/it/LC_MESSAGES/django.mo differ diff --git a/dj_rest_auth/locale/it/LC_MESSAGES/django.po b/dj_rest_auth/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..d901fa7 --- /dev/null +++ b/dj_rest_auth/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,123 @@ +# Italian language. +# 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: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-07 09:46+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Francesco Pinzauti \n" +"Language-Team: \n" +"Language: it\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:67 +msgid "View is not defined, pass it as a context variable" +msgstr "\"view\" non è definito, passalo come variabile di contesto" + +#: .\registration\serializers.py:72 +msgid "Define adapter_class in view" +msgstr "Definisci \"adapter_class\" in view" + +#: .\registration\serializers.py:91 +msgid "Define callback_url in view" +msgstr "Definisci \"callback_url\" in view" + +#: .\registration\serializers.py:95 +msgid "Define client_class in view" +msgstr "Definisci \"client_class\" in view" + +#: .\registration\serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "Input errato. È richiesto \"access_token\" o \"code\"." + +#: .\registration\serializers.py:125 +msgid "Incorrect value" +msgstr "Valore errato" + +#: .\registration\serializers.py:139 +msgid "User is already registered with this e-mail address." +msgstr "Un altro utente è già registrato con questo indirizzo e-mail." + +#: .\registration\serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "Un altro utente è già registrato con questo indirizzo e-mail." + +#: .\registration\serializers.py:193 +msgid "The two password fields didn't match." +msgstr "Le password non corrispondono." + +#: .\registration\views.py:47 +msgid "Verification e-mail sent." +msgstr "E-mail di verifica inviata." + +#: .\registration\views.py:95 +msgid "ok" +msgstr "ok" + +#: .\serializers.py:32 +msgid "Must include \"email\" and \"password\"." +msgstr "Deve includere \"email\" e \"password\"." + +#: .\serializers.py:43 +msgid "Must include \"username\" and \"password\"." +msgstr "Deve includere \"email\" e \"password\"." + +#: .\serializers.py:56 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Deve includere o \"username\" o \"email\" e \"password\"." + +#: .\serializers.py:97 +msgid "User account is disabled." +msgstr "L'account è disabilitato." + +#: .\serializers.py:100 +msgid "Unable to log in with provided credentials." +msgstr "Impossibile accedere con le credenziali fornite." + +#: .\serializers.py:109 +msgid "E-mail is not verified." +msgstr "L'e-mail non è verificata." + +#: .\serializers.py:264 +msgid "Your old password was entered incorrectly. Please enter it again." +msgstr "La tua password precedente non è corretta. Inseriscila nuovamente." + +#: .\views.py:139 +msgid "Successfully logged out." +msgstr "Disconnesso con successo." + +#: .\views.py:161 +msgid "Refresh token was not included in request data." +msgstr "Il \"Refresh token\" non è presente nei dati della richiesta." + +#: .\views.py:171 .\views.py:175 +msgid "An error has occurred." +msgstr "Si è verificato un errore." + +#: .\views.py:180 +msgid "" +"Neither cookies or blacklist are enabled, so the token has not been deleted " +"server side. Please make sure the token is deleted client side." +msgstr "" +"Né i cookies né la blacklist sono abilitati, quindi il token non è stato eliminato " +"lato server. Assicurarsi che il token sia eliminato lato client." + +#: .\views.py:230 +msgid "Password reset e-mail has been sent." +msgstr "L'e-mail per il recupero della password è stata inviata." + +#: .\views.py:256 +msgid "Password has been reset with the new password." +msgstr "Password aggiornata." + +#: .\views.py:278 +msgid "New password has been saved." +msgstr "Nuova password salvata." diff --git a/rest_auth/locale/ko/LC_MESSAGES/django.mo b/dj_rest_auth/locale/ko/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/ko/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/ko/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/ko/LC_MESSAGES/django.po b/dj_rest_auth/locale/ko/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/ko/LC_MESSAGES/django.po rename to dj_rest_auth/locale/ko/LC_MESSAGES/django.po diff --git a/rest_auth/locale/pl/LC_MESSAGES/django.mo b/dj_rest_auth/locale/pl/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/pl/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/pl/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/pl/LC_MESSAGES/django.po b/dj_rest_auth/locale/pl/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/pl/LC_MESSAGES/django.po rename to dj_rest_auth/locale/pl/LC_MESSAGES/django.po diff --git a/dj_rest_auth/locale/pt_BR/LC_MESSAGES/django.mo b/dj_rest_auth/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000..64a4e31 Binary files /dev/null and b/dj_rest_auth/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/pt_BR/LC_MESSAGES/django.po b/dj_rest_auth/locale/pt_BR/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/pt_BR/LC_MESSAGES/django.po rename to dj_rest_auth/locale/pt_BR/LC_MESSAGES/django.po diff --git a/rest_auth/locale/ru/LC_MESSAGES/django.mo b/dj_rest_auth/locale/ru/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/ru/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/ru/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/ru/LC_MESSAGES/django.po b/dj_rest_auth/locale/ru/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/ru/LC_MESSAGES/django.po rename to dj_rest_auth/locale/ru/LC_MESSAGES/django.po diff --git a/rest_auth/locale/tr/LC_MESSAGES/django.po b/dj_rest_auth/locale/tr/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/tr/LC_MESSAGES/django.po rename to dj_rest_auth/locale/tr/LC_MESSAGES/django.po diff --git a/dj_rest_auth/locale/uk/django.po b/dj_rest_auth/locale/uk/django.po new file mode 100644 index 0000000..519c59f --- /dev/null +++ b/dj_rest_auth/locale/uk/django.po @@ -0,0 +1,101 @@ +# 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: 2020-05-23 21:56-0800\n" +"PO-Revision-Date: 2020-05-23 00:56+0300\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" +"X-Generator: Poedit 2.3.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 "Встановіть adapter_class у view" + +#: registration/serializers.py:77 +msgid "Define callback_url in view" +msgstr "Встановіть callback_url у view" + +#: registration/serializers.py:81 +msgid "Define client_class in view" +msgstr "Встановіть client_class у view" + +#: 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 "Користувач з таким e-mail адресою вже зареєстрований." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Паролі не збігаються." + +#: registration/views.py:44 +msgid "Verification e-mail sent." +msgstr "Лист з підтвердженням вислано на email." + +#: registration/views.py:91 +msgid "ok" +msgstr "добре" + +#: serializers.py:30 +msgid "Must include \"email\" and \"password\"." +msgstr "Має включати \"email\" и \"пароль\"." + +#: serializers.py:41 +msgid "Must include \"username\" and \"password\"." +msgstr "Має включати \"юзернейм\" и \"пароль\"." + +#: serializers.py:54 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Повинно включати або \"юзернейм\" либо \"email\" и \"пароль\"." + +#: 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 "E-mail не підтверджено." + +#: 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/zh_Hans/LC_MESSAGES/django.mo b/dj_rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/zh_Hans/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/zh_Hans/LC_MESSAGES/django.po b/dj_rest_auth/locale/zh_Hans/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/zh_Hans/LC_MESSAGES/django.po rename to dj_rest_auth/locale/zh_Hans/LC_MESSAGES/django.po diff --git a/rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo b/dj_rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo similarity index 100% rename from rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo rename to dj_rest_auth/locale/zh_Hant/LC_MESSAGES/django.mo diff --git a/rest_auth/locale/zh_Hant/LC_MESSAGES/django.po b/dj_rest_auth/locale/zh_Hant/LC_MESSAGES/django.po similarity index 100% rename from rest_auth/locale/zh_Hant/LC_MESSAGES/django.po rename to dj_rest_auth/locale/zh_Hant/LC_MESSAGES/django.po diff --git a/dj_rest_auth/models.py b/dj_rest_auth/models.py new file mode 100644 index 0000000..8410821 --- /dev/null +++ b/dj_rest_auth/models.py @@ -0,0 +1,4 @@ +from django.conf import settings +from django.utils.module_loading import import_string + +TokenModel = import_string(getattr(settings, 'REST_AUTH_TOKEN_MODEL', 'rest_framework.authtoken.models.Token')) diff --git a/rest_auth/registration/__init__.py b/dj_rest_auth/registration/__init__.py similarity index 100% rename from rest_auth/registration/__init__.py rename to dj_rest_auth/registration/__init__.py diff --git a/rest_auth/registration/app_settings.py b/dj_rest_auth/registration/app_settings.py similarity index 58% rename from rest_auth/registration/app_settings.py rename to dj_rest_auth/registration/app_settings.py index c8cd25a..bbc7075 100644 --- a/rest_auth/registration/app_settings.py +++ b/dj_rest_auth/registration/app_settings.py @@ -1,19 +1,17 @@ +from dj_rest_auth.registration.serializers import \ + RegisterSerializer as DefaultRegisterSerializer from django.conf import settings - from rest_framework.permissions import AllowAny -from rest_auth.registration.serializers import ( - RegisterSerializer as DefaultRegisterSerializer) -from ..utils import import_callable +from ..utils import import_callable serializers = getattr(settings, 'REST_AUTH_REGISTER_SERIALIZERS', {}) -RegisterSerializer = import_callable( - serializers.get('REGISTER_SERIALIZER', DefaultRegisterSerializer)) +RegisterSerializer = import_callable(serializers.get('REGISTER_SERIALIZER', DefaultRegisterSerializer)) def register_permission_classes(): permission_classes = [AllowAny, ] for klass in getattr(settings, 'REST_AUTH_REGISTER_PERMISSION_CLASSES', tuple()): - permission_classes.append(import_callable(klass)) + permission_classes.append(klass) return tuple(permission_classes) diff --git a/rest_auth/registration/serializers.py b/dj_rest_auth/registration/serializers.py similarity index 99% rename from rest_auth/registration/serializers.py rename to dj_rest_auth/registration/serializers.py index 4f99c18..7d9d4a0 100644 --- a/rest_auth/registration/serializers.py +++ b/dj_rest_auth/registration/serializers.py @@ -1,6 +1,8 @@ +from django.contrib.auth import get_user_model from django.http import HttpRequest from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth import get_user_model +from requests.exceptions import HTTPError +from rest_framework import serializers try: from allauth.account import app_settings as allauth_settings @@ -14,9 +16,6 @@ try: except ImportError: raise ImportError("allauth needs to be added to INSTALLED_APPS.") -from rest_framework import serializers -from requests.exceptions import HTTPError - class SocialAccountSerializer(serializers.ModelSerializer): """ diff --git a/rest_auth/registration/urls.py b/dj_rest_auth/registration/urls.py similarity index 100% rename from rest_auth/registration/urls.py rename to dj_rest_auth/registration/urls.py index 1004695..c42cec6 100644 --- a/rest_auth/registration/urls.py +++ b/dj_rest_auth/registration/urls.py @@ -1,5 +1,5 @@ -from django.views.generic import TemplateView from django.conf.urls import url +from django.views.generic import TemplateView from .views import RegisterView, VerifyEmailView diff --git a/rest_auth/registration/views.py b/dj_rest_auth/registration/views.py similarity index 84% rename from rest_auth/registration/views.py rename to dj_rest_auth/registration/views.py index 1c28c16..02c73b9 100644 --- a/rest_auth/registration/views.py +++ b/dj_rest_auth/registration/views.py @@ -1,34 +1,30 @@ +from allauth.account import app_settings as allauth_settings +from allauth.account.adapter import get_adapter +from allauth.account.utils import complete_signup +from allauth.account.views import ConfirmEmailView +from allauth.socialaccount import signals +from allauth.socialaccount.adapter import get_adapter as get_social_adapter +from allauth.socialaccount.models import SocialAccount +from dj_rest_auth.app_settings import (JWTSerializer, TokenSerializer, + create_token) +from dj_rest_auth.models import TokenModel +from dj_rest_auth.registration.serializers import (SocialAccountSerializer, + SocialConnectSerializer, + SocialLoginSerializer, + VerifyEmailSerializer) +from dj_rest_auth.utils import jwt_encode +from dj_rest_auth.views import LoginView from django.conf import settings from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ 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, - IsAuthenticated) -from rest_framework.generics import CreateAPIView, ListAPIView, GenericAPIView -from rest_framework.exceptions import NotFound from rest_framework import status +from rest_framework.exceptions import NotFound +from rest_framework.generics import CreateAPIView, GenericAPIView, ListAPIView +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView -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 (TokenSerializer, - JWTSerializer, - create_token) -from rest_auth.models import TokenModel -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 sensitive_post_parameters_m = method_decorator( @@ -54,11 +50,12 @@ class RegisterView(CreateAPIView): if getattr(settings, 'REST_USE_JWT', False): data = { 'user': user, - 'token': self.token + 'access_token': self.access_token, + 'refresh_token': self.refresh_token } - return JWTSerializer(data).data + return JWTSerializer(data, context=self.get_serializer_context()).data else: - return TokenSerializer(user.auth_token).data + return TokenSerializer(user.auth_token, context=self.get_serializer_context()).data def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -73,7 +70,7 @@ class RegisterView(CreateAPIView): def perform_create(self, serializer): user = serializer.save(self.request) if getattr(settings, 'REST_USE_JWT', False): - self.token = jwt_encode(user) + self.access_token, self.refresh_token = jwt_encode(user) else: create_token(self.token_model, user, serializer) diff --git a/rest_auth/serializers.py b/dj_rest_auth/serializers.py similarity index 92% rename from rest_auth/serializers.py rename to dj_rest_auth/serializers.py index b645231..a65f765 100644 --- a/rest_auth/serializers.py +++ b/dj_rest_auth/serializers.py @@ -1,16 +1,18 @@ -from django.contrib.auth import get_user_model, authenticate from django.conf import settings +from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm from django.contrib.auth.tokens import default_token_generator -from django.utils.http import urlsafe_base64_decode as uid_decoder -from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text - -from rest_framework import serializers, exceptions +from django.utils.http import urlsafe_base64_decode as uid_decoder +from django.utils.module_loading import import_string +try: + from django.utils.translation import gettext_lazy as _ +except ImportError: + from django.utils.translation import ugettext_lazy as _ +from rest_framework import exceptions, serializers from rest_framework.exceptions import ValidationError from .models import TokenModel -from .utils import import_callable # Get the UserModel UserModel = get_user_model() @@ -102,7 +104,7 @@ class LoginSerializer(serializers.Serializer): raise exceptions.ValidationError(msg) # If required, is the email verified? - if 'rest_auth.registration' in settings.INSTALLED_APPS: + if 'dj_rest_auth.registration' in settings.INSTALLED_APPS: from allauth.account import app_settings if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: email_address = user.emailaddress_set.get(email=user.email) @@ -137,7 +139,8 @@ class JWTSerializer(serializers.Serializer): """ Serializer for JWT authentication. """ - token = serializers.CharField() + access_token = serializers.CharField() + refresh_token = serializers.CharField() user = serializers.SerializerMethodField() def get_user(self, obj): @@ -146,9 +149,14 @@ class JWTSerializer(serializers.Serializer): JWTSerializer. Defining it here to avoid circular imports """ rest_auth_serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) - JWTUserDetailsSerializer = import_callable( - rest_auth_serializers.get('USER_DETAILS_SERIALIZER', UserDetailsSerializer) + + JWTUserDetailsSerializer = import_string( + rest_auth_serializers.get( + 'USER_DETAILS_SERIALIZER', + 'dj_rest_auth.serializers.UserDetailsSerializer' + ) ) + user_data = JWTUserDetailsSerializer(obj['user'], context=self.context).data return user_data @@ -188,7 +196,7 @@ class PasswordResetSerializer(serializers.Serializer): class PasswordResetConfirmSerializer(serializers.Serializer): """ - Serializer for requesting a password reset e-mail. + Serializer for confirming a password reset attempt. """ new_password1 = serializers.CharField(max_length=128) new_password2 = serializers.CharField(max_length=128) @@ -210,6 +218,9 @@ class PasswordResetConfirmSerializer(serializers.Serializer): except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): raise ValidationError({'uid': ['Invalid value']}) + if not default_token_generator.check_token(self.user, attrs['token']): + raise ValidationError({'token': ['Invalid value']}) + self.custom_validation(attrs) # Construct SetPasswordForm instance self.set_password_form = self.set_password_form_class( @@ -217,8 +228,6 @@ class PasswordResetConfirmSerializer(serializers.Serializer): ) if not self.set_password_form.is_valid(): raise serializers.ValidationError(self.set_password_form.errors) - if not default_token_generator.check_token(self.user, attrs['token']): - raise ValidationError({'token': ['Invalid value']}) return attrs diff --git a/rest_auth/social_serializers.py b/dj_rest_auth/social_serializers.py similarity index 97% rename from rest_auth/social_serializers.py rename to dj_rest_auth/social_serializers.py index 1621813..60b0c2c 100644 --- a/rest_auth/social_serializers.py +++ b/dj_rest_auth/social_serializers.py @@ -1,6 +1,7 @@ from django.conf import settings from django.http import HttpRequest from rest_framework import serializers + # 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: @@ -8,7 +9,7 @@ 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 + from dj_rest_auth.registration.serializers import SocialConnectMixin class TwitterLoginSerializer(serializers.Serializer): diff --git a/rest_auth/tests/__init__.py b/dj_rest_auth/tests/__init__.py similarity index 100% rename from rest_auth/tests/__init__.py rename to dj_rest_auth/tests/__init__.py diff --git a/dj_rest_auth/tests/django_urls.py b/dj_rest_auth/tests/django_urls.py new file mode 100644 index 0000000..57f9d7d --- /dev/null +++ b/dj_rest_auth/tests/django_urls.py @@ -0,0 +1,45 @@ +# Moved in Django 1.8 from django to tests/auth_tests/urls.py + +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +from django.contrib.auth.urls import urlpatterns + +try: + from django.contrib.auth.views import ( + logout, login, password_reset, + password_change, password_reset_confirm + ) +except ImportError: + from django.contrib.auth.views import ( + LoginView, LogoutView, PasswordResetView, + PasswordChangeView, PasswordResetConfirmView + ) + logout = LogoutView.as_view() + login = LoginView.as_view() + password_reset = PasswordResetView.as_view() + password_change = PasswordChangeView.as_view() + password_reset_confirm = PasswordResetConfirmView.as_view() + + +# special urls for auth test cases +urlpatterns += [ + url(r'^logout/custom_query/$', logout, dict(redirect_field_name='follow')), + url(r'^logout/next_page/$', logout, dict(next_page='/somewhere/')), + url(r'^logout/next_page/named/$', logout, dict(next_page='password_reset')), + url(r'^password_reset_from_email/$', password_reset, dict(from_email='staffmember@example.com')), + url(r'^password_reset/custom_redirect/$', password_reset, dict(post_reset_redirect='/custom/')), + url(r'^password_reset/custom_redirect/named/$', password_reset, dict(post_reset_redirect='password_reset')), + url(r'^password_reset/html_email_template/$', password_reset, + dict(html_email_template_name='registration/html_password_reset_email.html')), + url(r'^reset/custom/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + password_reset_confirm, + dict(post_reset_redirect='/custom/')), + url(r'^reset/custom/named/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + password_reset_confirm, + dict(post_reset_redirect='password_reset')), + url(r'^password_change/custom/$', password_change, dict(post_change_redirect='/custom/')), + url(r'^password_change/custom/named/$', password_change, dict(post_change_redirect='password_reset')), + url(r'^admin_password_reset/$', password_reset, dict(is_admin_site=True)), + url(r'^login_required/$', login_required(password_reset)), + url(r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), +] diff --git a/rest_auth/tests/mixins.py b/dj_rest_auth/tests/mixins.py similarity index 96% rename from rest_auth/tests/mixins.py rename to dj_rest_auth/tests/mixins.py index 30b3d58..06d776d 100644 --- a/rest_auth/tests/mixins.py +++ b/dj_rest_auth/tests/mixins.py @@ -1,11 +1,9 @@ import json from django.conf import settings -from django.test.client import Client, MULTIPART_CONTENT +from django.test.client import MULTIPART_CONTENT, Client from django.utils.encoding import force_text - -from rest_framework import status -from rest_framework import permissions +from rest_framework import permissions, status try: from django.urls import reverse diff --git a/dj_rest_auth/tests/requirements.pip b/dj_rest_auth/tests/requirements.pip new file mode 100644 index 0000000..9f28d70 --- /dev/null +++ b/dj_rest_auth/tests/requirements.pip @@ -0,0 +1,4 @@ +django-allauth>=0.25.0 +responses>=0.3.0 +flake8==2.4.0 +djangorestframework-simplejwt==4.4.0 diff --git a/rest_auth/tests/settings.py b/dj_rest_auth/tests/settings.py similarity index 92% rename from rest_auth/tests/settings.py rename to dj_rest_auth/tests/settings.py index e353fb0..5f57f91 100644 --- a/rest_auth/tests/settings.py +++ b/dj_rest_auth/tests/settings.py @@ -1,8 +1,11 @@ +import logging import os import sys PROJECT_ROOT = os.path.abspath(os.path.split(os.path.split(__file__)[0])[0]) + +logging.disable(logging.CRITICAL) ROOT_URLCONF = 'urls' STATIC_URL = '/static/' STATIC_ROOT = '%s/staticserve' % PROJECT_ROOT @@ -65,11 +68,12 @@ TEMPLATES = [ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + 'dj_rest_auth.utils.JWTCookieAuthentication', ) } INSTALLED_APPS = [ + 'django.contrib.messages', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.humanize', @@ -88,10 +92,10 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework.authtoken', - 'rest_auth', - 'rest_auth.registration', + 'dj_rest_auth', + 'dj_rest_auth.registration', - 'rest_framework_jwt' + 'rest_framework_simplejwt.token_blacklist' ] SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" diff --git a/rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py similarity index 80% rename from rest_auth/tests/test_api.py rename to dj_rest_auth/tests/test_api.py index 9c5fd9e..c53be66 100644 --- a/rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -1,17 +1,17 @@ -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 +import json from allauth.account import app_settings as account_app_settings +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core import mail +from django.test import TestCase, override_settings +from django.utils.encoding import force_text 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 +from dj_rest_auth.registration.app_settings import register_permission_classes +from dj_rest_auth.registration.views import RegisterView +from .mixins import CustomPermissionClass, TestsMixin try: from django.urls import reverse @@ -154,8 +154,8 @@ class APIBasicTests(TestsMixin, TestCase): get_user_model().objects.create_user(self.USERNAME, '', self.PASS) self.post(self.login_url, data=payload, status_code=200) - self.assertEqual('token' in self.response.json.keys(), True) - self.token = self.response.json['token'] + self.assertEqual('access_token' in self.response.json.keys(), True) + self.token = self.response.json['access_token'] def test_login_by_email(self): # starting test without allauth app @@ -384,7 +384,7 @@ class APIBasicTests(TestsMixin, TestCase): "password": self.PASS } self.post(self.login_url, data=payload, status_code=200) - self.token = self.response.json['token'] + self.token = self.response.json['access_token'] self.get(self.user_url, status_code=200) self.patch(self.user_url, data=self.BASIC_USER_DATA, status_code=200) @@ -409,7 +409,6 @@ class APIBasicTests(TestsMixin, TestCase): @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 = () @@ -428,7 +427,7 @@ class APIBasicTests(TestsMixin, TestCase): self.post(self.register_url, data={}, status_code=400) result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) - self.assertIn('token', result.data) + self.assertIn('access_token', result.data) self.assertEqual(get_user_model().objects.all().count(), user_count + 1) self._login() @@ -479,7 +478,7 @@ class APIBasicTests(TestsMixin, TestCase): ) # verify email - email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL)\ + email_confirmation = new_user.emailaddress_set.get(email=self.EMAIL) \ .emailconfirmation_set.order_by('-created')[0] self.post( self.verify_email_url, @@ -516,3 +515,86 @@ class APIBasicTests(TestsMixin, TestCase): self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED) + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE='jwt-auth') + def test_login_jwt_sets_cookie(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + self.assertTrue('jwt-auth' in resp.cookies.keys()) + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE='jwt-auth') + def test_logout_jwt_deletes_cookie(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + self.post(self.login_url, data=payload, status_code=200) + resp = self.post(self.logout_url, status=200) + self.assertEqual('', resp.cookies.get('jwt-auth').value) + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE='jwt-auth') + @override_settings(REST_FRAMEWORK=dict( + DEFAULT_AUTHENTICATION_CLASSES=[ + 'dj_rest_auth.utils.JWTCookieAuthentication' + ] + )) + @override_settings(REST_SESSION_LOGIN=False) + def test_cookie_authentication(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + self.assertEqual(['jwt-auth'], list(resp.cookies.keys())) + resp = self.get('/protected-view/') + self.assertEquals(resp.status_code, 200) + + @override_settings(REST_USE_JWT=True) + def test_blacklisting_not_installed(self): + settings.INSTALLED_APPS.remove('rest_framework_simplejwt.token_blacklist') + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + token = resp.data['refresh_token'] + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.data["detail"], + "Neither cookies or blacklist are enabled, so the token has not been deleted server side. " + "Please make sure the token is deleted client side.") + + @override_settings(REST_USE_JWT=True) + def test_blacklisting(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + token = resp.data['refresh_token'] + # test refresh token not included in request data + resp = self.post(self.logout_url, status=200) + self.assertEqual(resp.status_code, 401) + # test token is invalid or expired + resp = self.post(self.logout_url, status=200, data={'refresh': '1'}) + self.assertEqual(resp.status_code, 401) + # test successful logout + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 200) + # test token is blacklisted + resp = self.post(self.logout_url, status=200, data={'refresh': token}) + self.assertEqual(resp.status_code, 401) + # test other TokenError, AttributeError, TypeError (invalid format) + resp = self.post(self.logout_url, status=200, data=json.dumps({'refresh': token})) + self.assertEqual(resp.status_code, 500) diff --git a/rest_auth/tests/test_social.py b/dj_rest_auth/tests/test_social.py similarity index 99% rename from rest_auth/tests/test_social.py rename to dj_rest_auth/tests/test_social.py index 830e631..819b153 100644 --- a/rest_auth/tests/test_social.py +++ b/dj_rest_auth/tests/test_social.py @@ -1,23 +1,21 @@ import json -from django.test import TestCase +import responses +from allauth.socialaccount.models import SocialApp +from allauth.socialaccount.providers.facebook.provider import GRAPH_API_URL from django.contrib.auth import get_user_model -from django.test.utils import override_settings from django.contrib.sites.models import Site +from django.test import TestCase +from django.test.utils import override_settings +from rest_framework import status + +from .mixins import TestsMixin 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 .mixins import TestsMixin - @override_settings(ROOT_URLCONF="tests.urls") class TestSocialAuth(TestsMixin, TestCase): @@ -304,7 +302,7 @@ class TestSocialAuth(TestsMixin, TestCase): } self.post(self.fb_login_url, data=payload, status_code=200) - self.assertIn('token', self.response.json.keys()) + self.assertIn('access_token', self.response.json.keys()) self.assertIn('user', self.response.json.keys()) self.assertEqual(get_user_model().objects.all().count(), users_count + 1) diff --git a/rest_auth/tests/urls.py b/dj_rest_auth/tests/urls.py similarity index 68% rename from rest_auth/tests/urls.py rename to dj_rest_auth/tests/urls.py index 401f23a..f1796d6 100644 --- a/rest_auth/tests/urls.py +++ b/dj_rest_auth/tests/urls.py @@ -1,20 +1,28 @@ -from django.conf.urls import url, include +from allauth.socialaccount.providers.facebook.views import \ + FacebookOAuth2Adapter +from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter +from dj_rest_auth.registration.views import (SocialAccountDisconnectView, + SocialAccountListView, + SocialConnectView, + SocialLoginView) +from dj_rest_auth.social_serializers import (TwitterConnectSerializer, + TwitterLoginSerializer) +from dj_rest_auth.urls import urlpatterns +from django.conf.urls import include, url from django.views.generic import TemplateView +from rest_framework import permissions +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.views import APIView + from . import django_urls -from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter -from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter -from rest_framework.decorators import api_view +class ExampleProtectedView(APIView): + permission_classes = [permissions.IsAuthenticated] -from rest_auth.urls import urlpatterns -from rest_auth.registration.views import ( - SocialLoginView, SocialConnectView, SocialAccountListView, - SocialAccountDisconnectView -) -from rest_auth.social_serializers import ( - TwitterLoginSerializer, TwitterConnectSerializer -) + def get(self, *args, **kwargs): + return Response(dict(success=True)) class FacebookLogin(SocialLoginView): @@ -53,7 +61,7 @@ class TwitterLoginNoAdapter(SocialLoginView): urlpatterns += [ - url(r'^rest-registration/', include('rest_auth.registration.urls')), + url(r'^rest-registration/', include('dj_rest_auth.registration.urls')), url(r'^test-admin/', include(django_urls)), url(r'^account-email-verification-sent/$', TemplateView.as_view(), name='account_email_verification_sent'), @@ -66,6 +74,7 @@ urlpatterns += [ 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'^protected-view/$', ExampleProtectedView.as_view()), 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/urls.py b/dj_rest_auth/urls.py similarity index 55% rename from rest_auth/urls.py rename to dj_rest_auth/urls.py index 7a35e9b..9c79691 100644 --- a/rest_auth/urls.py +++ b/dj_rest_auth/urls.py @@ -1,9 +1,8 @@ +from dj_rest_auth.views import (LoginView, LogoutView, PasswordChangeView, + PasswordResetConfirmView, PasswordResetView, + UserDetailsView) from django.conf.urls import url - -from rest_auth.views import ( - LoginView, LogoutView, UserDetailsView, PasswordChangeView, - PasswordResetView, PasswordResetConfirmView -) +from django.conf import settings urlpatterns = [ # URLs that do not require a session or valid token @@ -18,3 +17,13 @@ urlpatterns = [ url(r'^password/change/$', PasswordChangeView.as_view(), name='rest_password_change'), ] + +if getattr(settings, 'REST_USE_JWT', False): + from rest_framework_simplejwt.views import ( + TokenRefreshView, TokenVerifyView, + ) + + urlpatterns += [ + url(r'^token/verify/$', TokenVerifyView.as_view(), name='token_verify'), + url(r'^token/refresh/$', TokenRefreshView.as_view(), name='token_refresh'), + ] diff --git a/dj_rest_auth/utils.py b/dj_rest_auth/utils.py new file mode 100644 index 0000000..e1ef77a --- /dev/null +++ b/dj_rest_auth/utils.py @@ -0,0 +1,56 @@ +from importlib import import_module + + +def import_callable(path_or_callable): + if hasattr(path_or_callable, '__call__'): + return path_or_callable + else: + assert isinstance(path_or_callable, str) + package, attr = path_or_callable.rsplit('.', 1) + return getattr(import_module(package), attr) + + +def default_create_token(token_model, user, serializer): + token, _ = token_model.objects.get_or_create(user=user) + return token + + +def jwt_encode(user): + try: + from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + except ImportError: + raise ImportError("rest-framework-simplejwt needs to be installed") + + refresh = TokenObtainPairSerializer.get_token(user) + return refresh.access_token, refresh + + +try: + from rest_framework_simplejwt.authentication import JWTAuthentication + + class JWTCookieAuthentication(JWTAuthentication): + """ + An authentication plugin that hopefully authenticates requests through a JSON web + token provided in a request cookie (and through the header as normal, with a + preference to the header). + """ + def authenticate(self, request): + from django.conf import settings + cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) + header = self.get_header(request) + if header is None: + if cookie_name: + raw_token = request.COOKIES.get(cookie_name) + else: + return None + else: + raw_token = self.get_raw_token(header) + + if raw_token is None: + return None + + validated_token = self.get_validated_token(raw_token) + return self.get_user(validated_token), validated_token + +except ImportError: + pass diff --git a/rest_auth/views.py b/dj_rest_auth/views.py similarity index 67% rename from rest_auth/views.py rename to dj_rest_auth/views.py index 6e94a4c..e68b2ac 100644 --- a/rest_auth/views.py +++ b/dj_rest_auth/views.py @@ -1,25 +1,22 @@ -from django.contrib.auth import ( - login as django_login, - logout as django_logout -) from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth import login as django_login +from django.contrib.auth import logout as django_logout from django.core.exceptions import ObjectDoesNotExist from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ from django.views.decorators.debug import sensitive_post_parameters - from rest_framework import status -from rest_framework.views import APIView -from rest_framework.response import Response from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView -from .app_settings import ( - TokenSerializer, UserDetailsSerializer, LoginSerializer, - PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer, JWTSerializer, create_token -) +from .app_settings import (JWTSerializer, LoginSerializer, + PasswordChangeSerializer, + PasswordResetConfirmSerializer, + PasswordResetSerializer, TokenSerializer, + UserDetailsSerializer, create_token) from .models import TokenModel from .utils import jwt_encode @@ -63,7 +60,7 @@ class LoginView(GenericAPIView): self.user = self.serializer.validated_data['user'] if getattr(settings, 'REST_USE_JWT', False): - self.token = jwt_encode(self.user) + self.access_token, self.refresh_token = jwt_encode(self.user) else: self.token = create_token(self.token_model, self.user, self.serializer) @@ -77,7 +74,8 @@ class LoginView(GenericAPIView): if getattr(settings, 'REST_USE_JWT', False): data = { 'user': self.user, - 'token': self.token + 'access_token': self.access_token, + 'refresh_token': self.refresh_token } serializer = serializer_class(instance=data, context={'request': self.request}) @@ -87,14 +85,17 @@ class LoginView(GenericAPIView): 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: + cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) + from rest_framework_simplejwt.settings import api_settings as jwt_settings + if cookie_name: 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) + expiration = (datetime.utcnow() + jwt_settings.ACCESS_TOKEN_LIFETIME) + response.set_cookie( + cookie_name, + self.access_token, + expires=expiration, + httponly=True + ) return response def post(self, request, *args, **kwargs): @@ -129,15 +130,58 @@ class LogoutView(APIView): return self.logout(request) def logout(self, request): + try: + request.user.auth_token.delete() + except (AttributeError, ObjectDoesNotExist): + pass + if getattr(settings, 'REST_SESSION_LOGIN', True): django_logout(request) 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) + # NOTE: this import occurs here rather than at the top level + # because JWT support is optional, and if `REST_USE_JWT` isn't + # True we shouldn't need the dependency + from rest_framework_simplejwt.exceptions import TokenError + from rest_framework_simplejwt.tokens import RefreshToken + + cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) + if cookie_name: + response.delete_cookie(cookie_name) + + elif 'rest_framework_simplejwt.token_blacklist' in settings.INSTALLED_APPS: + # add refresh token to blacklist + try: + token = RefreshToken(request.data['refresh']) + token.blacklist() + + except KeyError: + response = Response({"detail": _("Refresh token was not included in request data.")}, + status=status.HTTP_401_UNAUTHORIZED) + + except (TokenError, AttributeError, TypeError) as error: + if hasattr(error, 'args'): + if 'Token is blacklisted' in error.args or 'Token is invalid or expired' in error.args: + response = Response({"detail": _(error.args[0])}, + status=status.HTTP_401_UNAUTHORIZED) + + else: + response = Response({"detail": _("An error has occurred.")}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + else: + response = Response({"detail": _("An error has occurred.")}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + else: + response = Response({ + "detail": _("Neither cookies or blacklist are enabled, so the token has not been deleted server " + "side. Please make sure the token is deleted client side." + )}, status=status.HTTP_200_OK) + return response @@ -162,7 +206,6 @@ class UserDetailsView(RetrieveUpdateAPIView): """ Adding this method since it is sometimes called when using django-rest-swagger - https://github.com/Tivix/django-rest-auth/issues/275 """ return get_user_model().objects.none() diff --git a/docs/Makefile b/docs/Makefile index cf09288..e71af70 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -85,17 +85,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-rest-auth.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dj-rest-auth.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-rest-auth.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dj-rest-auth.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/django-rest-auth" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-rest-auth" + @echo "# mkdir -p $$HOME/.local/share/devhelp/dj-rest-auth" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dj-rest-auth" @echo "# devhelp" epub: diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 46d2a05..dc159ce 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -4,7 +4,7 @@ API endpoints Basic ----- -- /rest-auth/login/ (POST) +- /dj-rest-auth/login/ (POST) - username - email @@ -12,24 +12,24 @@ Basic Returns Token key -- /rest-auth/logout/ (POST) +- /dj-rest-auth/logout/ (POST) .. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout -- /rest-auth/password/reset/ (POST) +- /dj-rest-auth/password/reset/ (POST) - email -- /rest-auth/password/reset/confirm/ (POST) +- /dj-rest-auth/password/reset/confirm/ (POST) - uid - token - new_password1 - new_password2 - .. note:: uid and token are sent in email after calling /rest-auth/password/reset/ + .. note:: uid and token are sent in email after calling /dj-rest-auth/password/reset/ -- /rest-auth/password/change/ (POST) +- /dj-rest-auth/password/change/ (POST) - new_password1 - new_password2 @@ -38,7 +38,7 @@ Basic .. note:: ``OLD_PASSWORD_FIELD_ENABLED = True`` to use old_password. .. note:: ``LOGOUT_ON_PASSWORD_CHANGE = False`` to keep the user logged in after password change -- /rest-auth/user/ (GET, PUT, PATCH) +- /dj-rest-auth/user/ (GET, PUT, PATCH) - username - first_name @@ -50,31 +50,36 @@ Basic Registration ------------ -- /rest-auth/registration/ (POST) +- /dj-rest-auth/registration/ (POST) - username - password1 - password2 - email -- /rest-auth/registration/verify-email/ (POST) +- /dj-rest-auth/registration/verify-email/ (POST) - key + .. note:: If you set account email verification as mandatory, you have to add the VerifyEmailView with the used `name`. + You need to import the view: ``from dj_rest_auth.registration.views import VerifyEmailView``. Then add the url with the corresponding name: + ``path('dj-rest-auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent')`` to the urlpatterns list. + + Social Media Authentication --------------------------- Basing on example from installation section :doc:`Installation ` -- /rest-auth/facebook/ (POST) +- /dj-rest-auth/facebook/ (POST) - access_token - code - .. note:: ``access_token`` OR ``code`` can be used as standalone arguments, see https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/registration/views.py + .. note:: ``access_token`` OR ``code`` can be used as standalone arguments, see https://github.com/jazzband/dj-rest-auth/blob/master/dj_rest_auth/registration/views.py -- /rest-auth/twitter/ (POST) +- /dj-rest-auth/twitter/ (POST) - access_token - token_secret diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index e7beb5d..0000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,120 +0,0 @@ -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 -- added sensitive fields decorator -- added Spanish translations - -0.9.0 ------ -- allowed using custom UserDetailsSerializer with JWTSerializer -- fixed error with logout on GET -- updated api endpoints and configuration docs -- bugfixes -- minor text fixes - -0.8.2 ------ -- fixed allauth import error -- added swagger docs to demo project - -0.8.1 ------ -- added support for django-allauth hmac email confirmation pattern - -0.8.0 ------ -- added support for django-rest-framework-jwt -- bugfixes - -0.7.0 ------ -- Wrapped API returned strings in ugettext_lazy -- Fixed not using ``get_username`` which caused issues when using custom user model without username field -- Django 1.9 support -- Added ``TwitterLoginSerializer`` - -0.6.0 ------ -- dropped support for Python 2.6 -- dropped support for Django 1.6 -- fixed demo code -- added better validation support for serializers -- added optional logout after password change -- compatibility fixes -- bugfixes - -0.5.0 ------ -- replaced request.DATA with request.data for compatibility with DRF 3.2 -- authorization codes for social login -- view classes rename (appended "View" to all of them) -- bugfixes - -0.4.0 ------ -- Django 1.8 compatiblity fixes - -0.3.4 ------ -- fixed bug in PasswordResetConfirmation serializer (token field wasn't validated) -- fixed bug in Register view - -0.3.3 ------ - -- support django-rest-framework v3.0 - -0.3.2 ------ - -- fixed few minor bugs - -0.3.1 ------ - -- added old_password field in PasswordChangeSerializer -- make all endpoints browsable -- removed LoggedInRESTAPIView, LoggedOutRESTAPIView -- fixed minor bugs - -0.3.0 ------ - -- replaced ``django-registration`` with ``django-allauth`` -- moved registration logic to separated django application (``rest_auth.registration``) -- added serializers customization in django settings -- added social media authentication view -- changed request method from GET to POST in logout endpoint -- changed request method from POST to PUT/PATCH for user details edition -- changed password reset confim url - uid and token should be sent in POST -- increase test coverage -- made compatibile with django 1.7 -- removed user profile support diff --git a/docs/conf.py b/docs/conf.py index 1c825cc..97831a7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# django-rest-auth documentation build configuration file, created by +# dj-rest-auth documentation build configuration file, created by # sphinx-quickstart on Wed Oct 8 15:59:37 2014. # # This file is execfile()d with the current directory set to its @@ -12,8 +12,12 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys + +about = {} +with open('../dj_rest_auth/__version__.py', 'r', encoding="utf8") as f: + exec(f.read(), about) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -43,17 +47,17 @@ source_suffix = '.rst' master_doc = 'index' # General information about the project. -project = u'django-rest-auth' -copyright = u'2018, Tivix Inc.' +project = u'dj-rest-auth' +copyright = u'2020, @iMerica' # 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.9.5' +version = about['__version__'] # The full version, including alpha/beta/rc tags. -release = '0.9.5' +release = about['__version__'] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -176,7 +180,7 @@ html_static_path = ['_static'] #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-rest-authdoc' +htmlhelp_basename = 'dj-rest-authdoc' # -- Options for LaTeX output --------------------------------------------- @@ -196,8 +200,8 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'django-rest-auth.tex', u'django-rest-auth Documentation', - u'Tivix Inc.', 'manual'), + ('index', 'dj-rest-auth.tex', u'dj-rest-auth Documentation', + u'iMerica Inc.', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -226,8 +230,8 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-rest-auth', u'django-rest-auth Documentation', - [u'Tivix Inc.'], 1) + ('index', 'dj-rest-auth', u'dj-rest-auth Documentation', + [u'@iMerica'], 1) ] # If true, show URL addresses after external links. @@ -240,8 +244,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-rest-auth', u'django-rest-auth Documentation', - u'Tivix Inc.', 'django-rest-auth', 'One line description of project.', + ('index', 'dj-rest-auth', u'dj-rest-auth Documentation', + u'@iMerica', 'dj-rest-auth', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/configuration.rst b/docs/configuration.rst index 59b301f..aac74c2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -6,19 +6,19 @@ Configuration You can define your custom serializers for each endpoint without overriding urls and views by adding ``REST_AUTH_SERIALIZERS`` dictionary in your django settings. Possible key values: - - LOGIN_SERIALIZER - serializer class in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.LoginSerializer`` + - LOGIN_SERIALIZER - serializer class in ``dj_rest_auth.views.LoginView``, default value ``dj_rest_auth.serializers.LoginSerializer`` - - TOKEN_SERIALIZER - response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.TokenSerializer`` + - TOKEN_SERIALIZER - response for successful authentication in ``dj_rest_auth.views.LoginView``, default value ``dj_rest_auth.serializers.TokenSerializer`` - - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.JWTSerializer`` + - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``dj_rest_auth.views.LoginView``, default value ``dj_rest_auth.serializers.JWTSerializer`` - - USER_DETAILS_SERIALIZER - serializer class in ``rest_auth.views.UserDetailsView``, default value ``rest_auth.serializers.UserDetailsSerializer`` + - USER_DETAILS_SERIALIZER - serializer class in ``dj_rest_auth.views.UserDetailsView``, default value ``dj_rest_auth.serializers.UserDetailsSerializer`` - - PASSWORD_RESET_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetView``, default value ``rest_auth.serializers.PasswordResetSerializer`` + - PASSWORD_RESET_SERIALIZER - serializer class in ``dj_rest_auth.views.PasswordResetView``, default value ``dj_rest_auth.serializers.PasswordResetSerializer`` - - PASSWORD_RESET_CONFIRM_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetConfirmView``, default value ``rest_auth.serializers.PasswordResetConfirmSerializer`` + - PASSWORD_RESET_CONFIRM_SERIALIZER - serializer class in ``dj_rest_auth.views.PasswordResetConfirmView``, default value ``dj_rest_auth.serializers.PasswordResetConfirmSerializer`` - - PASSWORD_CHANGE_SERIALIZER - serializer class in ``rest_auth.views.PasswordChangeView``, default value ``rest_auth.serializers.PasswordChangeSerializer`` + - PASSWORD_CHANGE_SERIALIZER - serializer class in ``dj_rest_auth.views.PasswordChangeView``, default value ``dj_rest_auth.serializers.PasswordChangeSerializer`` Example configuration: @@ -36,18 +36,18 @@ Configuration You can define your custom serializers for registration endpoint. Possible key values: - - REGISTER_SERIALIZER - serializer class in ``rest_auth.registration.views.RegisterView``, default value ``rest_auth.registration.serializers.RegisterSerializer`` + - REGISTER_SERIALIZER - serializer class in ``dj_rest_auth.registration.views.RegisterView``, default value ``dj_rest_auth.registration.serializers.RegisterSerializer`` .. note:: The custom REGISTER_SERIALIZER must define a ``def save(self, request)`` method that returns a user model instance -- **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models`` +- **REST_AUTH_TOKEN_MODEL** - path to model class for tokens, default value ``'rest_framework.authtoken.models.Token'`` -- **REST_AUTH_TOKEN_CREATOR** - callable to create tokens, default value ``rest_auth.utils.default_create_token``. +- **REST_AUTH_TOKEN_CREATOR** - path to callable or callable for creating tokens, default value ``dj_rest_auth.utils.default_create_token``. - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) -- **REST_USE_JWT** - Enable JWT Authentication instead of Token/Session based. This is built on top of django-rest-framework-jwt http://getblimp.github.io/django-rest-framework-jwt/, which must also be installed. (default: False) - +- **REST_USE_JWT** - Enable JWT Authentication instead of Token/Session based. This is built on top of djangorestframework-simplejwt https://github.com/SimpleJWT/django-rest-framework-simplejwt, which must also be installed. (default: False) +- **JWT_AUTH_COOKIE** - The cookie name/key. - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change diff --git a/docs/demo.rst b/docs/demo.rst index 877ca33..b8bc2f1 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -1,17 +1,28 @@ Demo project ============ -The idea of creating demo project was to show how you can potentially use -django-rest-auth app with jQuery on frontend. -Do these steps to make it running (ideally in virtualenv). +This demo project shows how you can potentially use +dj-rest-auth app with jQuery on frontend. +To run this locally follow the steps below. .. code-block:: python cd /tmp - git clone https://github.com/Tivix/django-rest-auth.git - cd django-rest-auth/demo/ + git clone https://github.com/jazzband/dj-rest-auth.git + cd dj-rest-auth/demo/ pip install -r requirements.pip python manage.py migrate --settings=demo.settings --noinput python manage.py runserver --settings=demo.settings -Now, go to ``http://127.0.0.1:8000/`` in your browser. + +Now, go to ``http://127.0.0.1:8000/`` in your browser. There is also a +Single Page Application (SPA) in React within the ``demo/`` directory. To run this do: + +.. code-block:: python + + cd react-spa/ + yarn # or npm install + yarn run start + + +Now, go to ``https://localhost:3000`` in your browser to view it. diff --git a/docs/disclosure.rst b/docs/disclosure.rst new file mode 100644 index 0000000..5b7619a --- /dev/null +++ b/docs/disclosure.rst @@ -0,0 +1,15 @@ +Vulnerability Disclosure Policy +=============================== + +Please observe the standard best practices of responsible disclosure, especially considering that this is OSS. +See OWASP's disclosure `cheat sheet `_. + +Some basic rules: + +- Keep it legal. +- Respect everyone's privacy. +- Contact the core maintainer(s) immediately if you discover a serious security vulnerability (imichael@pm.me for now). + + + + diff --git a/docs/faq.rst b/docs/faq.rst index d15566a..4a38211 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -3,7 +3,7 @@ FAQ 1. Why account_confirm_email url is defined but it is not usable? - In /rest_auth/registration/urls.py we can find something like this: + In /dj_rest_auth/registration/urls.py we can find something like this: .. code-block:: python @@ -36,12 +36,12 @@ FAQ # custom fields for user company_name = models.CharField(max_length=100) - To allow update user details within one request send to rest_auth.views.UserDetailsView view, create serializer like this: + To allow update user details within one request send to dj_rest_auth.views.UserDetailsView view, create serializer like this: .. code-block:: python from rest_framework import serializers - from rest_auth.serializers import UserDetailsSerializer + from dj_rest_auth.serializers import UserDetailsSerializer class UserSerializer(UserDetailsSerializer): diff --git a/docs/index.rst b/docs/index.rst index dc25e83..8569e55 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,17 @@ -.. django-rest-auth documentation master file, created by +.. dj-rest-auth documentation master file, created by sphinx-quickstart on Wed Oct 8 15:59:37 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to django-rest-auth's documentation! +Welcome to dj-rest-auth's documentation! ============================================ -.. warning:: Updating django-rest-auth from version **0.3.3** is highly recommended because of a security issue in PasswordResetConfirmation validation method. -.. note:: django-rest-auth from v0.3.3 supports django-rest-framework v3.0 +.. note:: dj-rest-auth version 1.0.0 now uses Django Simple JWT. -|build-status| |coverage-status| |requirements-status| |docs| +.. image:: https://circleci.com/gh/jazzband/dj-rest-auth.svg?style=svg + :target: https://circleci.com/gh/jazzband/dj-rest-auth Contents -------- @@ -25,28 +25,4 @@ Contents Configuration Demo project FAQ - Changelog - - -.. |build-status| image:: https://travis-ci.org/Tivix/django-rest-auth.svg?branch=master - :alt: build status - :scale: 100% - :target: https://travis-ci.org/Tivix/django-rest-auth - - -.. |coverage-status| image:: https://coveralls.io/repos/Tivix/django-rest-auth/badge.png?branch=master - :alt: coverage status - :scale: 100% - :target: https://coveralls.io/r/Tivix/django-rest-auth?branch=master - - -.. |requirements-status| image:: https://requires.io/github/Tivix/django-rest-auth/requirements.png?branch=master - :alt: requirements status - :scale: 100% - :target: https://requires.io/github/Tivix/django-rest-auth/requirements/?branch=master - - -.. |docs| image:: https://readthedocs.org/projects/django-rest-auth/badge/?version=latest - :scale: 100% - :target: https://readthedocs.org/projects/django-rest-auth/?badge=latest - :alt: Documentation Status + Disclosure Policy diff --git a/docs/installation.rst b/docs/installation.rst index b382d43..94579ee 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,9 +5,9 @@ Installation .. code-block:: python - pip install django-rest-auth + pip install dj-rest-auth -2. Add ``rest_auth`` app to INSTALLED_APPS in your django settings.py: +2. Add ``dj_rest_auth`` app to INSTALLED_APPS in your django settings.py: .. code-block:: python @@ -16,19 +16,19 @@ Installation 'rest_framework', 'rest_framework.authtoken', ..., - 'rest_auth' + 'dj_rest_auth' ) .. note:: This project depends on ``django-rest-framework`` library, so install it if you haven't done yet. Make sure also you have installed ``rest_framework`` and ``rest_framework.authtoken`` apps -3. Add rest_auth urls: +3. Add dj_rest_auth urls: .. code-block:: python urlpatterns = [ ..., - url(r'^rest-auth/', include('rest_auth.urls')) + path('dj-rest-auth/', include('dj_rest_auth.urls')) ] 4. Migrate your database @@ -44,9 +44,9 @@ You're good to go now! Registration (optional) ----------------------- -1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install django-rest-auth[with_social]``. +1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install 'dj-rest-auth[with_social]'``. -2. Add ``django.contrib.sites``, ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: +2. Add ``django.contrib.sites``, ``allauth``, ``allauth.account`` and ``dj_rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: 3. Add ``SITE_ID = 1`` to your django settings.py @@ -57,26 +57,26 @@ Registration (optional) 'django.contrib.sites', 'allauth', 'allauth.account', - 'rest_auth.registration', + 'dj_rest_auth.registration', ) SITE_ID = 1 -3. Add rest_auth.registration urls: +3. Add dj_rest_auth.registration urls: .. code-block:: python urlpatterns = [ ..., - url(r'^rest-auth/', include('rest_auth.urls')), - url(r'^rest-auth/registration/', include('rest_auth.registration.urls')) + path('dj-rest-auth/', include('dj_rest_auth.urls')), + path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')) ] Social Authentication (optional) -------------------------------- -Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. +Using ``django-allauth``, ``dj-rest-auth`` provides helpful class for creating social media authentication view. .. note:: Points 1 and 2 are related to ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 3. See ``django-allauth`` documentation for more details. @@ -88,12 +88,12 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati ..., 'rest_framework', 'rest_framework.authtoken', - 'rest_auth' + 'dj_rest_auth' ..., 'django.contrib.sites', 'allauth', 'allauth.account', - 'rest_auth.registration', + 'dj_rest_auth.registration', ..., 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', @@ -106,12 +106,12 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati Facebook ######## -3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute: +3. Create new view as a subclass of ``dj_rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute: .. code-block:: python from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - from rest_auth.registration.views import SocialLoginView + from dj_rest_auth.registration.views import SocialLoginView class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter @@ -122,7 +122,7 @@ Facebook urlpatterns += [ ..., - url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') + path('dj-rest-auth/facebook/', FacebookLogin.as_view(), name='fb_login') ] @@ -131,13 +131,13 @@ 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.registration.views.SocialLoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute: +3. Create new view as a subclass of ``dj_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.registration.views import SocialLoginView - from rest_auth.social_serializers import TwitterLoginSerializer + from dj_rest_auth.registration.views import SocialLoginView + from dj_rest_auth.social_serializers import TwitterLoginSerializer class TwitterLogin(SocialLoginView): serializer_class = TwitterLoginSerializer @@ -149,7 +149,7 @@ If you are using Twitter for your social authentication, it is a bit different s urlpatterns += [ ..., - url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') + path('dj-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. @@ -160,13 +160,13 @@ GitHub If you are using GitHub for your social authentication, it uses code and not AccessToken directly. -3. Create new view as a subclass of ``rest_auth.views.SocialLoginView`` with ``GitHubOAuth2Adapter`` adapter, an ``OAuth2Client`` and a callback_url as attributes: +3. Create new view as a subclass of ``dj_rest_auth.views.SocialLoginView`` with ``GitHubOAuth2Adapter`` adapter, an ``OAuth2Client`` and a callback_url as attributes: .. 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 + from dj_rest_auth.registration.views import SocialLoginView class GithubLogin(SocialLoginView): adapter_class = GitHubOAuth2Adapter @@ -179,7 +179,7 @@ If you are using GitHub for your social authentication, it uses code and not Acc urlpatterns += [ ..., - url(r'^rest-auth/github/$', GitHubLogin.as_view(), name='github_login') + path('dj-rest-auth/github/', GitHubLogin.as_view(), name='github_login') ] Additional Social Connect Views @@ -193,8 +193,8 @@ If you want to allow connecting existing accounts in addition to login, you can 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 + from dj_rest_auth.registration.views import SocialConnectView + from dj_rest_auth.social_serializers import TwitterConnectSerializer class FacebookConnect(SocialConnectView): adapter_class = FacebookOAuth2Adapter @@ -215,28 +215,28 @@ In urls.py: 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') + path('dj-rest-auth/facebook/connect/', FacebookConnect.as_view(), name='fb_connect') + path('dj-rest-auth/twitter/connect/', TwitterConnect.as_view(), name='twitter_connect') + path('dj-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 ( + from dj_rest_auth.registration.views import ( SocialAccountListView, SocialAccountDisconnectView ) urlpatterns += [ ..., - url( - r'^socialaccounts/$', + path( + 'socialaccounts/', SocialAccountListView.as_view(), name='social_account_list' ), - url( - r'^socialaccounts/(?P\d+)/disconnect/$', + path( + 'socialaccounts//disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect' ) @@ -246,15 +246,41 @@ You can also use the following views to check all social accounts attached to th 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: +By default ``dj-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. +1. Install `djangorestframework-simplejwt `_ + - ``djangorestframework-simplejwt`` is currently the only supported JWT library. -3. Add the following configuration value to your settings file to enable JWT authentication. +2. Add a simple_jwt auth configuration to the list of authentication classes. + +.. code-block:: python + + REST_FRAMEWORK = { + ... + 'DEFAULT_AUTHENTICATION_CLASSES': ( + ... + 'dj_rest_auth.utils.JWTCookieAuthentication', + ) + ... + } + +3. Add the following configuration value to your settings file to enable JWT authentication in dj-rest-auth. .. code-block:: python REST_USE_JWT = True + +4. Declare what you want the cookie key to be called. + +.. code-block:: python + + JWT_AUTH_COOKIE = 'my-app-auth' + + +This example value above will cause dj-rest-auth to return a `Set-Cookie` header that looks like this: + +.. code-block:: bash + + Set-Cookie: my-app-auth=xxxxxxxxxxxxx; expires=Sat, 28 Mar 2020 18:59:00 GMT; HttpOnly; Max-Age=300; Path=/ + +``JWT_AUTH_COOKIE`` is also used while authenticating each request against protected views. diff --git a/docs/introduction.rst b/docs/introduction.rst index 191abb5..6f4d354 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -2,7 +2,7 @@ Introduction ============ -Since the introduction of django-rest-framework, Django apps have been able to serve up app-level REST API endpoints. As a result, we saw a lot of instances where developers implemented their own REST registration API endpoints here and there, snippets, and so on. We aim to solve this demand by providing django-rest-auth, a set of REST API endpoints to handle User Registration and Authentication tasks. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for User Management. Of course, we'll add more API endpoints as we see the demand. +Since the introduction of django-rest-framework, Django apps have been able to serve up app-level REST API endpoints. As a result, we saw a lot of instances where developers implemented their own REST registration API endpoints here and there, snippets, and so on. We aim to solve this demand by providing dj-rest-auth, a set of REST API endpoints to handle User Registration and Authentication tasks. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for User Management. Of course, we'll add more API endpoints as we see the demand. Features -------- @@ -18,14 +18,14 @@ Features Apps structure -------------- -* ``rest_auth`` has basic auth functionality like login, logout, password reset and password change -* ``rest_auth.registration`` has logic related with registration and social media authentication +* ``dj_rest_auth`` has basic auth functionality like login, logout, password reset and password change +* ``dj_rest_auth.registration`` has logic related with registration and social media authentication Angular app ----------- -- Tivix has also created angular module which uses API endpoints from this app - `angular-django-registration-auth `_ +- iMerica has also created angular module which uses API endpoints from this app - `angular-django-registration-auth `_ Demo project diff --git a/docs/make.bat b/docs/make.bat index db1383d..3aff4bb 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -115,9 +115,9 @@ if "%1" == "qthelp" ( echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-rest-auth.qhcp + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\dj-rest-auth.qhcp echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-rest-auth.ghc + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\dj-rest-auth.ghc goto end ) diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py deleted file mode 100644 index 1b75fe6..0000000 --- a/rest_auth/app_settings.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.conf import settings - -from rest_auth.serializers import ( - TokenSerializer as DefaultTokenSerializer, - JWTSerializer as DefaultJWTSerializer, - UserDetailsSerializer as DefaultUserDetailsSerializer, - LoginSerializer as DefaultLoginSerializer, - PasswordResetSerializer as DefaultPasswordResetSerializer, - PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, - PasswordChangeSerializer as DefaultPasswordChangeSerializer) -from .utils import import_callable, default_create_token - -create_token = import_callable( - getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token)) - -serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) - -TokenSerializer = import_callable( - serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) - -JWTSerializer = import_callable( - serializers.get('JWT_SERIALIZER', DefaultJWTSerializer)) - -UserDetailsSerializer = import_callable( - serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) -) - -LoginSerializer = import_callable( - serializers.get('LOGIN_SERIALIZER', DefaultLoginSerializer) -) - -PasswordResetSerializer = import_callable( - serializers.get( - 'PASSWORD_RESET_SERIALIZER', - DefaultPasswordResetSerializer - ) -) - -PasswordResetConfirmSerializer = import_callable( - serializers.get( - 'PASSWORD_RESET_CONFIRM_SERIALIZER', - DefaultPasswordResetConfirmSerializer - ) -) - -PasswordChangeSerializer = import_callable( - serializers.get( - 'PASSWORD_CHANGE_SERIALIZER', - DefaultPasswordChangeSerializer - ) -) diff --git a/rest_auth/locale/sv/LC_MESSAGES/django.mo b/rest_auth/locale/sv/LC_MESSAGES/django.mo new file mode 100644 index 0000000..185c73c Binary files /dev/null and b/rest_auth/locale/sv/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/sv/LC_MESSAGES/django.po b/rest_auth/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 0000000..747f5af --- /dev/null +++ b/rest_auth/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,107 @@ +# 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: 2020-05-10 12:48+0000\n" +"PO-Revision-Date: 2020-05-10 15:04+0200\n" +"Language: sv\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.4\n" + +#: rest_auth/rest_auth/registration/serializers.py:67 +msgid "View is not defined, pass it as a context variable" +msgstr "Vyn definieras inte, skicka den som en kontextvariabel" + +#: rest_auth/rest_auth/registration/serializers.py:72 +msgid "Define adapter_class in view" +msgstr "Definiera “adapter_class” i vyn" + +#: rest_auth/rest_auth/registration/serializers.py:91 +msgid "Define callback_url in view" +msgstr "Definiera “callback_url” i vyn" + +#: rest_auth/rest_auth/registration/serializers.py:95 +msgid "Define client_class in view" +msgstr "Definiera “client_class” i vyn" + +#: rest_auth/rest_auth/registration/serializers.py:116 +msgid "Incorrect input. access_token or code is required." +msgstr "Felaktig inmatning. access_token eller code krävs." + +#: rest_auth/rest_auth/registration/serializers.py:125 +msgid "Incorrect value" +msgstr "Felaktigt värde" + +#: rest_auth/rest_auth/registration/serializers.py:139 +msgid "User is already registered with this e-mail address." +msgstr "Användaren är redan registrerad med den här e-postadressen." + +#: rest_auth/rest_auth/registration/serializers.py:185 +msgid "A user is already registered with this e-mail address." +msgstr "En användare är redan registrerad med den här e-postadressen." + +#: rest_auth/rest_auth/registration/serializers.py:193 +msgid "The two password fields didn't match." +msgstr "De två lösenordsfälten matchade inte." + +#: rest_auth/rest_auth/registration/views.py:51 +msgid "Verification e-mail sent." +msgstr "Verifikationsmail har skickats." + +#: rest_auth/rest_auth/registration/views.py:98 +msgid "ok" +msgstr "ok" + +#: rest_auth/rest_auth/serializers.py:33 +msgid "Must include \"email\" and \"password\"." +msgstr "Måste inkludera “email” och “password”." + +#: rest_auth/rest_auth/serializers.py:44 +msgid "Must include \"username\" and \"password\"." +msgstr "Måste innehålla “username” och “password”." + +#: rest_auth/rest_auth/serializers.py:57 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Måste innehålla antingen “username” eller “email” och “password”." + +#: rest_auth/rest_auth/serializers.py:98 +msgid "User account is disabled." +msgstr "Användarkontot är avstängt." + +#: rest_auth/rest_auth/serializers.py:101 +msgid "Unable to log in with provided credentials." +msgstr "Det går inte att logga in med de angivna uppgifterna." + +#: rest_auth/rest_auth/serializers.py:110 +msgid "E-mail is not verified." +msgstr "E-post är inte verifierad." + +#: rest_auth/rest_auth/serializers.py:259 +msgid "Your old password was entered incorrectly. Please enter it again." +msgstr "Ditt gamla lösenord angavs felaktigt. Vänligen ange det igen." + +#: rest_auth/rest_auth/views.py:137 +msgid "Successfully logged out." +msgstr "Utloggad." + +#: rest_auth/rest_auth/views.py:190 +msgid "Password reset e-mail has been sent." +msgstr "E-post för återställning av lösenord har skickats." + +#: rest_auth/rest_auth/views.py:216 +msgid "Password has been reset with the new password." +msgstr "Lösenordet har återställts med det nya lösenordet." + +#: rest_auth/rest_auth/views.py:238 +msgid "New password has been saved." +msgstr "Nytt lösenord har sparats." diff --git a/rest_auth/models.py b/rest_auth/models.py deleted file mode 100644 index a132f9c..0000000 --- a/rest_auth/models.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf import settings - -from rest_framework.authtoken.models import Token as DefaultTokenModel - -from .utils import import_callable - -# Register your models here. - -TokenModel = import_callable( - getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel)) diff --git a/rest_auth/tests/django_urls.py b/rest_auth/tests/django_urls.py deleted file mode 100644 index c1fb050..0000000 --- a/rest_auth/tests/django_urls.py +++ /dev/null @@ -1,30 +0,0 @@ -# Moved in Django 1.8 from django to tests/auth_tests/urls.py - -from django.conf.urls import url -from django.contrib.auth import views -from django.contrib.auth.decorators import login_required -from django.contrib.auth.urls import urlpatterns - - -# special urls for auth test cases -urlpatterns += [ - url(r'^logout/custom_query/$', views.logout, dict(redirect_field_name='follow')), - url(r'^logout/next_page/$', views.logout, dict(next_page='/somewhere/')), - url(r'^logout/next_page/named/$', views.logout, dict(next_page='password_reset')), - url(r'^password_reset_from_email/$', views.password_reset, dict(from_email='staffmember@example.com')), - url(r'^password_reset/custom_redirect/$', views.password_reset, dict(post_reset_redirect='/custom/')), - url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')), - url(r'^password_reset/html_email_template/$', views.password_reset, - dict(html_email_template_name='registration/html_password_reset_email.html')), - url(r'^reset/custom/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - views.password_reset_confirm, - dict(post_reset_redirect='/custom/')), - url(r'^reset/custom/named/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', - views.password_reset_confirm, - dict(post_reset_redirect='password_reset')), - url(r'^password_change/custom/$', views.password_change, dict(post_change_redirect='/custom/')), - url(r'^password_change/custom/named/$', views.password_change, dict(post_change_redirect='password_reset')), - url(r'^admin_password_reset/$', views.password_reset, dict(is_admin_site=True)), - url(r'^login_required/$', login_required(views.password_reset)), - url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')), -] diff --git a/rest_auth/tests/requirements.pip b/rest_auth/tests/requirements.pip deleted file mode 100644 index f48ee3c..0000000 --- a/rest_auth/tests/requirements.pip +++ /dev/null @@ -1,5 +0,0 @@ -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/utils.py b/rest_auth/utils.py deleted file mode 100644 index 800f184..0000000 --- a/rest_auth/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -from six import string_types -from importlib import import_module - - -def import_callable(path_or_callable): - if hasattr(path_or_callable, '__call__'): - return path_or_callable - else: - assert isinstance(path_or_callable, string_types) - package, attr = path_or_callable.rsplit('.', 1) - return getattr(import_module(package), attr) - - -def default_create_token(token_model, user, serializer): - token, _ = token_model.objects.get_or_create(user=user) - return token - - -def jwt_encode(user): - try: - from rest_framework_jwt.settings import api_settings - except ImportError: - raise ImportError("djangorestframework_jwt needs to be installed") - - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - - payload = jwt_payload_handler(user) - return jwt_encode_handler(payload) diff --git a/runtests.py b/runtests.py index 8b7ede2..3cb605b 100644 --- a/runtests.py +++ b/runtests.py @@ -3,13 +3,14 @@ import os import sys -os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' -test_dir = os.path.join(os.path.dirname(__file__), 'rest_auth') +import django +from django.conf import settings +from django.test.utils import get_runner + +os.environ['DJANGO_SETTINGS_MODULE'] = 'dj_rest_auth.tests.settings' +test_dir = os.path.join(os.path.dirname(__file__), 'dj_rest_auth') sys.path.insert(0, test_dir) -import django -from django.test.utils import get_runner -from django.conf import settings def runtests(): @@ -17,8 +18,9 @@ def runtests(): test_runner = TestRunner(verbosity=1, interactive=True) if hasattr(django, 'setup'): django.setup() - failures = test_runner.run_tests(['rest_auth']) + failures = test_runner.run_tests(['dj_rest_auth']) sys.exit(bool(failures)) + if __name__ == '__main__': runtests() diff --git a/setup.py b/setup.py index 675b99d..fac004b 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,34 @@ #!/usr/bin/env python import os -from setuptools import setup, find_packages +from setuptools import find_packages, setup here = os.path.dirname(os.path.abspath(__file__)) -f = open(os.path.join(here, 'README.rst')) +f = open(os.path.join(here, 'README.md')) long_description = f.read().strip() f.close() +about = {} +with open('dj_rest_auth/__version__.py', 'r', encoding="utf8") as f: + exec(f.read(), about) + setup( - name='django-rest-auth', - version='0.9.5', - author='Sumit Chachra', - author_email='chachra@tivix.com', - url='http://github.com/Tivix/django-rest-auth', - description='Create a set of REST API endpoints for Authentication and Registration', + name='dj-rest-auth', + version=about['__version__'], + author='iMerica', + author_email='imichael@pm.me', + url='http://github.com/jazzband/dj-rest-auth', + description='Authentication and Registration in Django Rest Framework', packages=find_packages(), long_description=long_description, + long_description_content_type='text/markdown', keywords='django rest auth registration rest-framework django-registration api', zip_safe=False, install_requires=[ - 'Django>=1.8.0', + 'Django>=1.11', 'djangorestframework>=3.1.3', - 'six>=1.9.0', ], extras_require={ 'with_social': ['django-allauth>=0.25.0'], @@ -32,10 +36,12 @@ setup( tests_require=[ 'responses>=0.5.0', 'django-allauth>=0.25.0', - 'djangorestframework-jwt>=1.9.0', + 'djangorestframework-simplejwt>=4.4.0 ', + 'coveralls>=1.11.1' ], test_suite='runtests.runtests', include_package_data=True, + python_requires='>=3.5', classifiers=[ 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..484802b --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +# tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +# Running this tox will test against all supported version +# combinations of python and django as described at the following +# https://docs.djangoproject.com/en/3.0/faq/install/#what-python-version-can-i-use-with-django +# https://endoflife.date/django +[tox] +skipsdist = true +envlist = + python{3.5,3.6,3.7,3.8}-django22 + python{3.6,3.7,3.8}-django30 + +[testenv] +commands = + python ./runtests.py +deps = + -rdev-requirements.txt + django22: Django>=2.2,<2.3 + django30: Django>=3.0,<3.1 + +# Configuration for coverage and flake8 is being set in `./setup.cfg` +[testenv:coverage] +commands = + coverage run ./runtests.py + coverage report +deps = + -rdev-requirements.txt + +[testenv:flake8] +changedir = {toxinidir}/dj_rest_auth +commands = + flake8 . +deps = + flake8==3.8.1