kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Merge branch '1108-session-auth' into 'develop'
Support session/cookie based auth, see #1108 See merge request funkwhale/funkwhale!1126plugins-v3
commit
70c5b3be62
|
@ -77,9 +77,11 @@ v1_patterns += [
|
||||||
r"^history/",
|
r"^history/",
|
||||||
include(("funkwhale_api.history.urls", "history"), namespace="history"),
|
include(("funkwhale_api.history.urls", "history"), namespace="history"),
|
||||||
),
|
),
|
||||||
|
url(r"^", include(("funkwhale_api.users.api_urls", "users"), namespace="users"),),
|
||||||
|
# XXX: 1.0: remove this
|
||||||
url(
|
url(
|
||||||
r"^users/",
|
r"^users/",
|
||||||
include(("funkwhale_api.users.api_urls", "users"), namespace="users"),
|
include(("funkwhale_api.users.api_urls", "users"), namespace="users-nested"),
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^oauth/",
|
r"^oauth/",
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
from funkwhale_api.common.auth import TokenAuthMiddleware
|
from funkwhale_api.common.auth import TokenAuthMiddleware
|
||||||
from funkwhale_api.instance import consumers
|
from funkwhale_api.instance import consumers
|
||||||
|
|
||||||
application = ProtocolTypeRouter(
|
application = ProtocolTypeRouter(
|
||||||
{
|
{
|
||||||
# Empty for now (http->django views is added by default)
|
# Empty for now (http->django views is added by default)
|
||||||
"websocket": TokenAuthMiddleware(
|
"websocket": AuthMiddlewareStack(
|
||||||
URLRouter([url("^api/v1/activity$", consumers.InstanceActivityConsumer)])
|
TokenAuthMiddleware(
|
||||||
|
URLRouter(
|
||||||
|
[url("^api/v1/activity$", consumers.InstanceActivityConsumer)]
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -276,10 +276,12 @@ MIDDLEWARE = tuple(ADDITIONAL_MIDDLEWARES_BEFORE) + (
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"corsheaders.middleware.CorsMiddleware",
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
"funkwhale_api.common.middleware.SPAFallbackMiddleware",
|
# needs to be before SPA middleware
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
# /end
|
||||||
|
"funkwhale_api.common.middleware.SPAFallbackMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"funkwhale_api.users.middleware.RecordActivityMiddleware",
|
"funkwhale_api.users.middleware.RecordActivityMiddleware",
|
||||||
|
@ -998,6 +1000,10 @@ THROTTLING_RATES = {
|
||||||
"rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"),
|
"rate": THROTTLING_USER_RATES.get("oauth-revoke-token", "100/hour"),
|
||||||
"description": "OAuth token deletion",
|
"description": "OAuth token deletion",
|
||||||
},
|
},
|
||||||
|
"login": {
|
||||||
|
"rate": THROTTLING_USER_RATES.get("login", "30/hour"),
|
||||||
|
"description": "Login",
|
||||||
|
},
|
||||||
"jwt-login": {
|
"jwt-login": {
|
||||||
"rate": THROTTLING_USER_RATES.get("jwt-login", "30/hour"),
|
"rate": THROTTLING_USER_RATES.get("jwt-login", "30/hour"),
|
||||||
"description": "JWT token creation",
|
"description": "JWT token creation",
|
||||||
|
|
|
@ -29,6 +29,9 @@ class TokenAuthMiddleware:
|
||||||
self.inner = inner
|
self.inner = inner
|
||||||
|
|
||||||
def __call__(self, scope):
|
def __call__(self, scope):
|
||||||
|
if "user" in scope:
|
||||||
|
# auth already handled
|
||||||
|
return self.inner(scope)
|
||||||
# XXX: 1.0 remove this, replace with websocket/scopedtoken
|
# XXX: 1.0 remove this, replace with websocket/scopedtoken
|
||||||
auth = TokenHeaderAuth()
|
auth = TokenHeaderAuth()
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -10,6 +10,7 @@ import xml.sax.saxutils
|
||||||
from django import http
|
from django import http
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
|
from django.middleware import csrf
|
||||||
from django import urls
|
from django import urls
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
|
|
||||||
|
@ -81,7 +82,12 @@ def serve_spa(request):
|
||||||
body, tail = tail.split("</body>", 1)
|
body, tail = tail.split("</body>", 1)
|
||||||
css = "<style>{}</style>".format(css)
|
css = "<style>{}</style>".format(css)
|
||||||
tail = body + "\n" + css + "\n</body>" + tail
|
tail = body + "\n" + css + "\n</body>" + tail
|
||||||
return http.HttpResponse(head + tail)
|
|
||||||
|
# set a csrf token so that visitor can login / query API if needed
|
||||||
|
token = csrf.get_token(request)
|
||||||
|
response = http.HttpResponse(head + tail)
|
||||||
|
response.set_cookie("csrftoken", token, max_age=None)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_LINK_REGEX = re.compile(r"<link [^>]*rel=(?:'|\")?manifest(?:'|\")?[^>]*>")
|
MANIFEST_LINK_REGEX = re.compile(r"<link [^>]*rel=(?:'|\")?manifest(?:'|\")?[^>]*>")
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
from django.conf.urls import url
|
||||||
from funkwhale_api.common import routers
|
from funkwhale_api.common import routers
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
router = routers.OptionalSlashRouter()
|
router = routers.OptionalSlashRouter()
|
||||||
router.register(r"users", views.UserViewSet, "users")
|
router.register(r"users", views.UserViewSet, "users")
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = [
|
||||||
|
url(r"^users/login/?$", views.login, name="login"),
|
||||||
|
url(r"^users/logout/?$", views.logout, name="logout"),
|
||||||
|
] + router.urls
|
||||||
|
|
|
@ -4,6 +4,8 @@ from django.core import validators
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib import auth
|
||||||
|
|
||||||
from rest_auth.serializers import PasswordResetSerializer as PRS
|
from rest_auth.serializers import PasswordResetSerializer as PRS
|
||||||
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -265,3 +267,23 @@ class UserDeleteSerializer(serializers.Serializer):
|
||||||
if not value:
|
if not value:
|
||||||
raise serializers.ValidationError("Please confirm deletion")
|
raise serializers.ValidationError("Please confirm deletion")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField()
|
||||||
|
password = serializers.CharField()
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
user = auth.authenticate(request=self.context.get("request"), **data)
|
||||||
|
if not user:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Unable to log in with provided credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise serializers.ValidationError("This account was disabled")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def save(self, request):
|
||||||
|
return auth.login(request, self.validated_data)
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django import http
|
||||||
|
from django.contrib import auth
|
||||||
|
from django.middleware import csrf
|
||||||
|
|
||||||
from allauth.account.adapter import get_adapter
|
from allauth.account.adapter import get_adapter
|
||||||
from rest_auth import views as rest_auth_views
|
from rest_auth import views as rest_auth_views
|
||||||
from rest_auth.registration import views as registration_views
|
from rest_auth.registration import views as registration_views
|
||||||
from rest_framework import mixins, viewsets
|
from rest_framework import mixins
|
||||||
|
from rest_framework import viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from funkwhale_api.common import authentication
|
from funkwhale_api.common import authentication
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
from funkwhale_api.common import throttling
|
||||||
|
|
||||||
from . import models, serializers, tasks
|
from . import models, serializers, tasks
|
||||||
|
|
||||||
|
@ -105,3 +113,26 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
|
||||||
if not self.request.user.username == kwargs.get("username"):
|
if not self.request.user.username == kwargs.get("username"):
|
||||||
return Response(status=403)
|
return Response(status=403)
|
||||||
return super().partial_update(request, *args, **kwargs)
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def login(request):
|
||||||
|
throttling.check_request(request, "login")
|
||||||
|
if request.method != "POST":
|
||||||
|
return http.HttpResponse(status=405)
|
||||||
|
serializer = serializers.LoginSerializer(
|
||||||
|
data=request.POST, context={"request": request}
|
||||||
|
)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return http.HttpResponse(
|
||||||
|
json.dumps(serializer.errors), status=400, content_type="application/json"
|
||||||
|
)
|
||||||
|
serializer.save(request)
|
||||||
|
csrf.rotate_token(request)
|
||||||
|
return http.HttpResponse(status=200)
|
||||||
|
|
||||||
|
|
||||||
|
def logout(request):
|
||||||
|
if request.method != "POST":
|
||||||
|
return http.HttpResponse(status=405)
|
||||||
|
auth.logout(request)
|
||||||
|
return http.HttpResponse(status=200)
|
||||||
|
|
|
@ -14,7 +14,7 @@ from funkwhale_api.common import utils
|
||||||
def test_spa_fallback_middleware_no_404(mocker):
|
def test_spa_fallback_middleware_no_404(mocker):
|
||||||
get_response = mocker.Mock()
|
get_response = mocker.Mock()
|
||||||
get_response.return_value = mocker.Mock(status_code=200)
|
get_response.return_value = mocker.Mock(status_code=200)
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
m = middleware.SPAFallbackMiddleware(get_response)
|
m = middleware.SPAFallbackMiddleware(get_response)
|
||||||
|
|
||||||
assert m(request) == get_response.return_value
|
assert m(request) == get_response.return_value
|
||||||
|
@ -26,7 +26,7 @@ def test_spa_middleware_calls_should_fallback_false(mocker):
|
||||||
should_falback = mocker.patch.object(
|
should_falback = mocker.patch.object(
|
||||||
middleware, "should_fallback_to_spa", return_value=False
|
middleware, "should_fallback_to_spa", return_value=False
|
||||||
)
|
)
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
|
|
||||||
m = middleware.SPAFallbackMiddleware(get_response)
|
m = middleware.SPAFallbackMiddleware(get_response)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ def test_spa_middleware_calls_should_fallback_false(mocker):
|
||||||
def test_spa_middleware_should_fallback_true(mocker):
|
def test_spa_middleware_should_fallback_true(mocker):
|
||||||
get_response = mocker.Mock()
|
get_response = mocker.Mock()
|
||||||
get_response.return_value = mocker.Mock(status_code=404)
|
get_response.return_value = mocker.Mock(status_code=404)
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
mocker.patch.object(middleware, "should_fallback_to_spa", return_value=True)
|
mocker.patch.object(middleware, "should_fallback_to_spa", return_value=True)
|
||||||
serve_spa = mocker.patch.object(middleware, "serve_spa")
|
serve_spa = mocker.patch.object(middleware, "serve_spa")
|
||||||
m = middleware.SPAFallbackMiddleware(get_response)
|
m = middleware.SPAFallbackMiddleware(get_response)
|
||||||
|
@ -56,7 +56,7 @@ def test_should_fallback(path, expected, mocker):
|
||||||
|
|
||||||
def test_serve_spa_from_cache(mocker, settings, preferences, no_api_auth):
|
def test_serve_spa_from_cache(mocker, settings, preferences, no_api_auth):
|
||||||
preferences["instance__name"] = 'Best Funkwhale "pod"'
|
preferences["instance__name"] = 'Best Funkwhale "pod"'
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
get_spa_html = mocker.patch.object(
|
get_spa_html = mocker.patch.object(
|
||||||
middleware,
|
middleware,
|
||||||
"get_spa_html",
|
"get_spa_html",
|
||||||
|
@ -155,7 +155,7 @@ def test_get_route_head_tags(mocker, settings):
|
||||||
|
|
||||||
|
|
||||||
def test_serve_spa_includes_custom_css(mocker, no_api_auth):
|
def test_serve_spa_includes_custom_css(mocker, no_api_auth):
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
middleware,
|
middleware,
|
||||||
"get_spa_html",
|
"get_spa_html",
|
||||||
|
@ -178,6 +178,23 @@ def test_serve_spa_includes_custom_css(mocker, no_api_auth):
|
||||||
assert response.content == "\n".join(expected).encode()
|
assert response.content == "\n".join(expected).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_serve_spa_sets_csrf_token(mocker, no_api_auth):
|
||||||
|
request = mocker.Mock(path="/", META={})
|
||||||
|
get_token = mocker.patch.object(middleware.csrf, "get_token", return_value="test")
|
||||||
|
mocker.patch.object(
|
||||||
|
middleware,
|
||||||
|
"get_spa_html",
|
||||||
|
return_value="<html><head></head><body></body></html>",
|
||||||
|
)
|
||||||
|
mocker.patch.object(middleware, "get_default_head_tags", return_value=[])
|
||||||
|
mocker.patch.object(middleware, "get_request_head_tags", return_value=[])
|
||||||
|
response = middleware.serve_spa(request)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
get_token.assert_called_once_with(request)
|
||||||
|
assert response.cookies["csrftoken"].value == get_token.return_value
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"custom_css, expected",
|
"custom_css, expected",
|
||||||
[
|
[
|
||||||
|
@ -281,7 +298,7 @@ def test_rewrite_manifest_json_url(link, new_url, expected, mocker, settings):
|
||||||
spa_html = "<html><head><link rel=before>{}<link rel=after></head></html>".format(
|
spa_html = "<html><head><link rel=before>{}<link rel=after></head></html>".format(
|
||||||
link
|
link
|
||||||
)
|
)
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
middleware, "get_default_head_tags", return_value=[],
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
@ -299,7 +316,7 @@ def test_rewrite_manifest_json_url_rewrite_disabled(mocker, settings):
|
||||||
settings.FUNKWHALE_SPA_REWRITE_MANIFEST = False
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST = False
|
||||||
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = "custom_url"
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = "custom_url"
|
||||||
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
middleware, "get_default_head_tags", return_value=[],
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
@ -318,7 +335,7 @@ def test_rewrite_manifest_json_url_rewrite_default_url(mocker, settings):
|
||||||
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = None
|
settings.FUNKWHALE_SPA_REWRITE_MANIFEST_URL = None
|
||||||
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
spa_html = "<html><head><link href=/manifest.json rel=manifest></head></html>"
|
||||||
expected_url = federation_utils.full_url(reverse("api:v1:instance:spa-manifest"))
|
expected_url = federation_utils.full_url(reverse("api:v1:instance:spa-manifest"))
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
mocker.patch.object(middleware, "get_spa_html", return_value=spa_html)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
middleware, "get_default_head_tags", return_value=[],
|
middleware, "get_default_head_tags", return_value=[],
|
||||||
|
@ -342,7 +359,7 @@ def test_spa_middleware_handles_api_redirect(mocker):
|
||||||
match = mocker.Mock(args=["hello"], kwargs={"foo": "bar"}, func=api_view)
|
match = mocker.Mock(args=["hello"], kwargs={"foo": "bar"}, func=api_view)
|
||||||
mocker.patch.object(middleware.urls, "resolve", return_value=match)
|
mocker.patch.object(middleware.urls, "resolve", return_value=match)
|
||||||
|
|
||||||
request = mocker.Mock(path="/")
|
request = mocker.Mock(path="/", META={})
|
||||||
|
|
||||||
m = middleware.SPAFallbackMiddleware(get_response)
|
m = middleware.SPAFallbackMiddleware(get_response)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from django.test import Client
|
||||||
|
|
||||||
from funkwhale_api.common import serializers as common_serializers
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.moderation import tasks as moderation_tasks
|
from funkwhale_api.moderation import tasks as moderation_tasks
|
||||||
|
@ -518,3 +520,39 @@ def test_user_login_jwt_honor_email_verification(
|
||||||
url = reverse("api:v1:token")
|
url = reverse("api:v1:token")
|
||||||
response = api_client.post(url, data)
|
response = api_client.post(url, data)
|
||||||
assert response.status_code == expected_status_code
|
assert response.status_code == expected_status_code
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_via_api(api_client, factories):
|
||||||
|
user = factories["users.User"]()
|
||||||
|
url = reverse("api:v1:users:login")
|
||||||
|
payload = {"username": user.username, "password": "test"}
|
||||||
|
|
||||||
|
response = api_client.post(url, payload)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert api_client.session["_auth_user_id"] == str(user.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_via_api_inactive(api_client, factories):
|
||||||
|
user = factories["users.User"](is_active=False)
|
||||||
|
url = reverse("api:v1:users:login")
|
||||||
|
payload = {"username": user.username, "password": "test"}
|
||||||
|
|
||||||
|
response = api_client.post(url, payload)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_via_api_no_csrf(factories):
|
||||||
|
user = factories["users.User"]()
|
||||||
|
url = reverse("api:v1:users:login")
|
||||||
|
payload = {"username": user.username, "password": "test"}
|
||||||
|
api_client = Client(enforce_csrf_checks=True)
|
||||||
|
response = api_client.post(url, payload)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout(api_client, factories, mocker):
|
||||||
|
auth_logout = mocker.patch("django.contrib.auth.logout")
|
||||||
|
url = reverse("api:v1:users:logout")
|
||||||
|
response = api_client.post(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert auth_logout.call_count == 1
|
||||||
|
|
|
@ -5,7 +5,8 @@ RUN curl -L -o /usr/local/bin/jq https://github.com/stedolan/jq/releases/downloa
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
ADD package.json yarn.lock ./
|
COPY scripts/ ./scripts/
|
||||||
|
ADD package.json yarn.lock ./
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
|
@ -68,7 +68,10 @@ Vue.directive('dropdown', function (el, binding) {
|
||||||
...(binding.value || {})
|
...(binding.value || {})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||||
|
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
|
||||||
axios.interceptors.request.use(function (config) {
|
axios.interceptors.request.use(function (config) {
|
||||||
|
|
||||||
// Do something before request is sent
|
// Do something before request is sent
|
||||||
if (store.state.auth.token) {
|
if (store.state.auth.token) {
|
||||||
config.headers['Authorization'] = store.getters['auth/header']
|
config.headers['Authorization'] = store.getters['auth/header']
|
||||||
|
@ -84,7 +87,7 @@ axios.interceptors.response.use(function (response) {
|
||||||
return response
|
return response
|
||||||
}, function (error) {
|
}, function (error) {
|
||||||
error.backendErrors = []
|
error.backendErrors = []
|
||||||
if (error.response.status === 401) {
|
if (store.state.auth.authenticated && error.response.status === 401) {
|
||||||
store.commit('auth/authenticated', false)
|
store.commit('auth/authenticated', false)
|
||||||
logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath)
|
logger.default.warn('Received 401 response from API, redirecting to login form', router.currentRoute.fullPath)
|
||||||
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
router.push({name: 'login', query: {next: router.currentRoute.fullPath}})
|
||||||
|
|
|
@ -89,9 +89,13 @@ export default {
|
||||||
actions: {
|
actions: {
|
||||||
// Send a request to the login URL and save the returned JWT
|
// Send a request to the login URL and save the returned JWT
|
||||||
login ({commit, dispatch}, {next, credentials, onError}) {
|
login ({commit, dispatch}, {next, credentials, onError}) {
|
||||||
return axios.post('token/', credentials).then(response => {
|
var form = new FormData();
|
||||||
|
Object.keys(credentials).forEach((k) => {
|
||||||
|
form.set(k, credentials[k])
|
||||||
|
})
|
||||||
|
return axios.post('users/login', form).then(response => {
|
||||||
logger.default.info('Successfully logged in as', credentials.username)
|
logger.default.info('Successfully logged in as', credentials.username)
|
||||||
commit('token', response.data.token)
|
// commit('token', response.data.token)
|
||||||
dispatch('fetchProfile').then(() => {
|
dispatch('fetchProfile').then(() => {
|
||||||
// Redirect to a specified route
|
// Redirect to a specified route
|
||||||
router.push(next)
|
router.push(next)
|
||||||
|
@ -101,7 +105,8 @@ export default {
|
||||||
onError(response)
|
onError(response)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
logout ({commit}) {
|
async logout ({commit}) {
|
||||||
|
await axios.post('users/logout')
|
||||||
let modules = [
|
let modules = [
|
||||||
'auth',
|
'auth',
|
||||||
'favorites',
|
'favorites',
|
||||||
|
@ -116,16 +121,14 @@ export default {
|
||||||
logger.default.info('Log out, goodbye!')
|
logger.default.info('Log out, goodbye!')
|
||||||
router.push({name: 'index'})
|
router.push({name: 'index'})
|
||||||
},
|
},
|
||||||
check ({commit, dispatch, state}) {
|
async check ({commit, dispatch, state}) {
|
||||||
logger.default.info('Checking authentication…')
|
logger.default.info('Checking authentication…')
|
||||||
var jwt = state.token
|
commit('authenticated', false)
|
||||||
if (jwt) {
|
let profile = await dispatch('fetchProfile')
|
||||||
commit('token', jwt)
|
if (profile) {
|
||||||
dispatch('fetchProfile')
|
commit('authenticated', true)
|
||||||
dispatch('refreshToken')
|
|
||||||
} else {
|
} else {
|
||||||
logger.default.info('Anonymous user')
|
logger.default.info('Anonymous user')
|
||||||
commit('authenticated', false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchProfile ({commit, dispatch, state}) {
|
fetchProfile ({commit, dispatch, state}) {
|
||||||
|
@ -174,13 +177,5 @@ export default {
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshToken ({commit, dispatch, state}) {
|
|
||||||
return axios.post('token/refresh/', {token: state.token}).then(response => {
|
|
||||||
logger.default.info('Refreshed auth token')
|
|
||||||
commit('token', response.data.token)
|
|
||||||
}, response => {
|
|
||||||
logger.default.error('Error while refreshing token', response.data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,20 +91,11 @@ describe('store/auth', () => {
|
||||||
action: store.actions.check,
|
action: store.actions.check,
|
||||||
params: {state: {}},
|
params: {state: {}},
|
||||||
expectedMutations: [
|
expectedMutations: [
|
||||||
{ type: 'authenticated', payload: false }
|
{ type: 'authenticated', payload: false },
|
||||||
]
|
{ type: 'authenticated', payload: true },
|
||||||
})
|
|
||||||
})
|
|
||||||
it('check jwt set', () => {
|
|
||||||
testAction({
|
|
||||||
action: store.actions.check,
|
|
||||||
params: {state: {token: 'test', username: 'user'}},
|
|
||||||
expectedMutations: [
|
|
||||||
{ type: 'token', payload: 'test' }
|
|
||||||
],
|
],
|
||||||
expectedActions: [
|
expectedActions: [
|
||||||
{ type: 'fetchProfile' },
|
{ type: 'fetchProfile' },
|
||||||
{ type: 'refreshToken' }
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -173,18 +164,5 @@ describe('store/auth', () => {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('refreshToken', () => {
|
|
||||||
moxios.stubRequest('token/refresh/', {
|
|
||||||
status: 200,
|
|
||||||
response: {token: 'newtoken'}
|
|
||||||
})
|
|
||||||
testAction({
|
|
||||||
action: store.actions.refreshToken,
|
|
||||||
params: {state: {token: 'oldtoken'}},
|
|
||||||
expectedMutations: [
|
|
||||||
{ type: 'token', payload: 'newtoken' }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Ładowanie…
Reference in New Issue