mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-04 13:15:24 +00:00
implemented authentication endpoints, user login and registration
This commit is contained in:
parent
0a884058df
commit
977af261b6
53 changed files with 1689 additions and 722 deletions
.gitlab-ci.ymlMakefile
backend
manage.py
requirements
src/baserow
tests
sandbox
web-frontend
|
@ -35,6 +35,13 @@ backend-flake8:
|
|||
backend-pytest:
|
||||
stage: test
|
||||
image: python:3.6
|
||||
services:
|
||||
- name: postgres:11.3
|
||||
alias: db
|
||||
variables:
|
||||
POSTGRES_USER: baserow
|
||||
POSTGRES_PASSWORD: baserow
|
||||
POSTGRES_DB: baserow
|
||||
script:
|
||||
- make install-backend-dependencies
|
||||
- export PYTHONPATH=$CI_PROJECT_DIR/backend/src
|
||||
|
|
6
Makefile
6
Makefile
|
@ -13,7 +13,7 @@ install-backend-dependencies:
|
|||
pip install -r backend/requirements/base.txt
|
||||
pip install -r backend/requirements/dev.txt
|
||||
|
||||
install-dependencies: backend-dependencies install-web-frontend-dependencies
|
||||
install-dependencies: install-backend-dependencies install-web-frontend-dependencies
|
||||
|
||||
eslint-web-frontend:
|
||||
(cd web-frontend && yarn run eslint) || exit;
|
||||
|
@ -31,3 +31,7 @@ lint-backend:
|
|||
|
||||
test-backend:
|
||||
(cd backend && pytest tests) || exit;
|
||||
|
||||
lint: lint-backend lint-web-frontend
|
||||
test: test-backend test-web-frontend
|
||||
lint-and-test: lint test
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'baserow.config.settings.dev')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,2 +1,3 @@
|
|||
flake8==3.7.7
|
||||
pytest-django==3.5.0
|
||||
pytest-django>=3.5.0
|
||||
Faker==1.0.7
|
||||
|
|
1
backend/src/baserow/api/v0/__init__.py
Normal file
1
backend/src/baserow/api/v0/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
app_name = 'baserow.api.v0'
|
5
backend/src/baserow/api/v0/apps.py
Normal file
5
backend/src/baserow/api/v0/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = 'baserow.api.v0'
|
56
backend/src/baserow/api/v0/decorators.py
Normal file
56
backend/src/baserow/api/v0/decorators.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
def map_exceptions(exceptions):
|
||||
"""This decorator easily maps specific exceptions to a standard api response.
|
||||
|
||||
Example:
|
||||
@map_exceptions({ SomeException: 'ERROR_1' })
|
||||
def get(self, request):
|
||||
raise SomeException('This is a test')
|
||||
|
||||
HTTP/1.1 400
|
||||
{
|
||||
"error": "ERROR_1",
|
||||
"detail": "This is a test"
|
||||
}
|
||||
|
||||
Example 2:
|
||||
@map_exceptions({ SomeException: ('ERROR_1', 404, 'Other message') })
|
||||
def get(self, request):
|
||||
raise SomeException('This is a test')
|
||||
|
||||
HTTP/1.1 404
|
||||
{
|
||||
"error": "ERROR_1",
|
||||
"detail": "Other message"
|
||||
}
|
||||
"""
|
||||
def map_exceptions_decorator(func):
|
||||
def func_wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except tuple(exceptions.keys()) as e:
|
||||
value = exceptions.get(e.__class__)
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
detail = str(e)
|
||||
|
||||
if isinstance(value, str):
|
||||
error = value
|
||||
if isinstance(value, tuple):
|
||||
error = value[0]
|
||||
if len(value) > 1 and value[1] is not None:
|
||||
status_code = value[1]
|
||||
if len(value) > 2 and value[2] is not None:
|
||||
detail = value[2]
|
||||
|
||||
exc = APIException({
|
||||
'error': error,
|
||||
'detail': detail
|
||||
})
|
||||
exc.status_code = status_code
|
||||
|
||||
raise exc
|
||||
return func_wrapper
|
||||
return map_exceptions_decorator
|
10
backend/src/baserow/api/v0/urls.py
Normal file
10
backend/src/baserow/api/v0/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path, include
|
||||
|
||||
from .user import urls as user_urls
|
||||
|
||||
|
||||
app_name = 'baserow.api.v0'
|
||||
|
||||
urlpatterns = [
|
||||
path('user/', include(user_urls, namespace='user'))
|
||||
]
|
8
backend/src/baserow/api/v0/user/jwt.py
Normal file
8
backend/src/baserow/api/v0/user/jwt.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from .serializers import UserSerializer
|
||||
|
||||
|
||||
def jwt_response_payload_handler(token, user=None, request=None):
|
||||
return {
|
||||
'token': token,
|
||||
'user': UserSerializer(user, context={'request': request}).data
|
||||
}
|
22
backend/src/baserow/api/v0/user/serializers.py
Normal file
22
backend/src/baserow/api/v0/user/serializers.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'username', 'password')
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
'write_only': True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=32)
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(max_length=32)
|
||||
authenticate = serializers.BooleanField(required=False, default=False)
|
17
backend/src/baserow/api/v0/user/urls.py
Normal file
17
backend/src/baserow/api/v0/user/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from rest_framework_jwt.views import (
|
||||
obtain_jwt_token, refresh_jwt_token, verify_jwt_token
|
||||
)
|
||||
|
||||
from .views import UserView
|
||||
|
||||
|
||||
app_name = 'baserow.api.v0.user'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^token-auth/$', obtain_jwt_token, name='token_auth'),
|
||||
url(r'^token-refresh/$', refresh_jwt_token, name='token_refresh'),
|
||||
url(r'^token-verify/$', verify_jwt_token, name='token_verify'),
|
||||
url(r'^$', UserView.as_view(), name='index')
|
||||
]
|
43
backend/src/baserow/api/v0/user/views.py
Normal file
43
backend/src/baserow/api/v0/user/views.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from django.db import transaction
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
from baserow.api.v0.decorators import map_exceptions
|
||||
from baserow.user.handler import UserHandler
|
||||
from baserow.user.exceptions import UserAlreadyExist
|
||||
|
||||
|
||||
from .serializers import RegisterSerializer, UserSerializer
|
||||
|
||||
|
||||
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
|
||||
|
||||
|
||||
class UserView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
user_handler = UserHandler()
|
||||
|
||||
@transaction.atomic
|
||||
@map_exceptions({
|
||||
UserAlreadyExist: 'ERROR_ALREADY_EXISTS'
|
||||
})
|
||||
def post(self, request):
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.data
|
||||
user = self.user_handler.create_user(name=data['name'], email=data['email'],
|
||||
password=data['password'])
|
||||
|
||||
response = {'user': UserSerializer(user).data}
|
||||
|
||||
if data['authenticate']:
|
||||
payload = jwt_payload_handler(user)
|
||||
token = jwt_encode_handler(payload)
|
||||
response.update(token=token)
|
||||
|
||||
return Response(response)
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import datetime
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
@ -21,10 +22,14 @@ INSTALLED_APPS = [
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'rest_framework'
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
|
||||
'baserow.api.v0'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
|
@ -118,3 +123,15 @@ REST_FRAMEWORK = {
|
|||
'rest_framework.authentication.BasicAuthentication',
|
||||
),
|
||||
}
|
||||
|
||||
CORS_ORIGIN_WHITELIST = (
|
||||
'http://localhost:3000',
|
||||
)
|
||||
|
||||
JWT_AUTH = {
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
|
||||
'JWT_ALLOW_REFRESH': True,
|
||||
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||
'JWT_RESPONSE_PAYLOAD_HANDLER': 'baserow.api.v0.user.jwt.'
|
||||
'jwt_response_payload_handler'
|
||||
}
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
from django.urls import path, include
|
||||
from django.urls import include
|
||||
from django.conf.urls import url
|
||||
|
||||
from rest_framework import routers
|
||||
from rest_framework_jwt.views import obtain_jwt_token
|
||||
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^api/token-auth/', obtain_jwt_token),
|
||||
path('api/', include(router.urls)),
|
||||
url(r'^api/v0/', include('baserow.api.v0.urls', namespace='api')),
|
||||
]
|
||||
|
|
0
backend/src/baserow/user/__init__.py
Normal file
0
backend/src/baserow/user/__init__.py
Normal file
2
backend/src/baserow/user/exceptions.py
Normal file
2
backend/src/baserow/user/exceptions.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
class UserAlreadyExist(Exception):
|
||||
pass
|
28
backend/src/baserow/user/handler.py
Normal file
28
backend/src/baserow/user/handler.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import IntegrityError
|
||||
|
||||
from .exceptions import UserAlreadyExist
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserHandler:
|
||||
def create_user(self, name, email, password):
|
||||
"""Create a new user with the provided information.
|
||||
|
||||
:param name: The name of the new user.
|
||||
:param email: The e-mail address of the user, this is also the username.
|
||||
:param password: The password of the user.
|
||||
:return: The user object.
|
||||
:rtype: User
|
||||
"""
|
||||
|
||||
try:
|
||||
user = User(first_name=name, email=email, username=email)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
raise UserAlreadyExist(f'A user with username {email} already exists.')
|
||||
|
||||
return user
|
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
75
backend/tests/baserow/api/v0/user/test_token_auth.py
Normal file
75
backend/tests/baserow/api/v0/user/test_token_auth.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import pytest
|
||||
from unittest.mock import patch
|
||||
from datetime import datetime
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
|
||||
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_auth(client, data_fixture):
|
||||
data_fixture.create_user(email='test@test.nl', password='password',
|
||||
first_name='Test1')
|
||||
|
||||
response = client.post(reverse('api:user:token_auth'), {
|
||||
'username': 'no_existing@test.nl',
|
||||
'password': 'password'
|
||||
})
|
||||
json = response.json()
|
||||
|
||||
assert response.status_code == 400
|
||||
assert len(json['non_field_errors']) > 0
|
||||
|
||||
response = client.post(reverse('api:user:token_auth'), {
|
||||
'username': 'test@test.nl',
|
||||
'password': 'wrong_password'
|
||||
})
|
||||
json = response.json()
|
||||
|
||||
assert response.status_code == 400
|
||||
assert len(json['non_field_errors']) > 0
|
||||
|
||||
response = client.post(reverse('api:user:token_auth'), {
|
||||
'username': 'test@test.nl',
|
||||
'password': 'password'
|
||||
})
|
||||
json = response.json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert 'token' in json
|
||||
assert 'user' in json
|
||||
assert json['user']['username'] == 'test@test.nl'
|
||||
assert json['user']['first_name'] == 'Test1'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_refresh(client, data_fixture):
|
||||
user = data_fixture.create_user(email='test@test.nl', password='password',
|
||||
first_name='Test1')
|
||||
|
||||
response = client.post(reverse('api:user:token_refresh'), {'token': 'WRONG_TOKEN'})
|
||||
assert response.status_code == 400
|
||||
|
||||
payload = jwt_payload_handler(user)
|
||||
token = jwt_encode_handler(payload)
|
||||
|
||||
response = client.post(reverse('api:user:token_refresh'), {'token': token})
|
||||
assert response.status_code == 200
|
||||
assert 'token' in response.json()
|
||||
|
||||
with patch('rest_framework_jwt.utils.datetime') as mock_datetime:
|
||||
mock_datetime.utcnow.return_value = datetime(2019, 1, 1, 1, 1, 1, 0)
|
||||
payload = jwt_payload_handler(user)
|
||||
token = jwt_encode_handler(payload)
|
||||
|
||||
response = client.post(reverse('api:user:token_refresh'), {'token': token})
|
||||
assert response.status_code == 400
|
37
backend/tests/baserow/api/v0/user/test_user_views.py
Normal file
37
backend/tests/baserow/api/v0/user/test_user_views.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import pytest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.shortcuts import reverse
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_user(client):
|
||||
response = client.post(reverse('api:user:index'), {
|
||||
'name': 'Test1',
|
||||
'email': 'test@test.nl',
|
||||
'password': 'test12'
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
user = User.objects.get(email='test@test.nl')
|
||||
assert user.first_name == 'Test1'
|
||||
assert user.email == 'test@test.nl'
|
||||
assert user.password != ''
|
||||
|
||||
response_failed = client.post(reverse('api:user:index'), {
|
||||
'name': 'Test1',
|
||||
'email': 'test@test.nl',
|
||||
'password': 'test12'
|
||||
})
|
||||
|
||||
assert response_failed.status_code == 400
|
||||
assert response_failed.json()['error'] == 'ERROR_ALREADY_EXISTS'
|
||||
|
||||
response_failed_2 = client.post(reverse('api:user:index'), {
|
||||
'email': 'test'
|
||||
})
|
||||
|
||||
assert response_failed_2.status_code == 400
|
|
@ -1,4 +0,0 @@
|
|||
def test_homepage(client):
|
||||
response = client.get('/api/')
|
||||
assert response.json()['detail'] == 'Authentication credentials were not provided.'
|
||||
assert response.status_code == 401
|
18
backend/tests/baserow/user/test_user_handler.py
Normal file
18
backend/tests/baserow/user/test_user_handler.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from baserow.user.exceptions import UserAlreadyExist
|
||||
from baserow.user.handler import UserHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_user():
|
||||
user_handler = UserHandler()
|
||||
|
||||
user = user_handler.create_user('Test1', 'test@test.nl', 'password')
|
||||
assert user.pk
|
||||
assert user.first_name == 'Test1'
|
||||
assert user.email == 'test@test.nl'
|
||||
assert user.username == 'test@test.nl'
|
||||
|
||||
with pytest.raises(UserAlreadyExist):
|
||||
user_handler.create_user('Test1', 'test@test.nl', 'password')
|
7
backend/tests/conftest.py
Normal file
7
backend/tests/conftest.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data_fixture():
|
||||
from .fixtures import Fixtures
|
||||
return Fixtures()
|
5
backend/tests/fixtures/__init__.py
vendored
Normal file
5
backend/tests/fixtures/__init__.py
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
from .user import UserFixtures
|
||||
|
||||
|
||||
class Fixtures(UserFixtures):
|
||||
pass
|
20
backend/tests/fixtures/user.py
vendored
Normal file
20
backend/tests/fixtures/user.py
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from faker import Faker
|
||||
|
||||
|
||||
fake = Faker()
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class UserFixtures:
|
||||
def create_user(self, **kwargs):
|
||||
kwargs.setdefault('email', fake.email())
|
||||
kwargs.setdefault('username', kwargs['email'])
|
||||
kwargs.setdefault('first_name', fake.name())
|
||||
kwargs.setdefault('password', 'password')
|
||||
|
||||
user = User(**kwargs)
|
||||
user.set_password(kwargs['password'])
|
||||
user.save()
|
||||
|
||||
return user
|
|
@ -935,7 +935,7 @@
|
|||
webpack-node-externals "^1.7.2"
|
||||
webpackbar "^3.2.0"
|
||||
|
||||
"@nuxtjs/axios@^5.3.6":
|
||||
"@nuxtjs/axios@^5.5.4":
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@nuxtjs/axios/-/axios-5.5.4.tgz#c4aee2322901b19d4072670c03144662a73ea6f4"
|
||||
integrity sha512-/Ljsyh5VIc9paXGrQue7RQ+PpBNES1oit0g4l+ya1tfyKnZMpHSbghuLcv0xq+BpXlSEr690uemHbz54/N6U5w==
|
||||
|
@ -962,6 +962,11 @@
|
|||
mustache "^2.3.0"
|
||||
stack-trace "0.0.10"
|
||||
|
||||
"@types/cookie@^0.3.1":
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
|
||||
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
|
||||
|
||||
"@types/q@^1.5.1":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
|
@ -2186,6 +2191,22 @@ cookie-signature@1.0.6:
|
|||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie-universal-nuxt@^2.0.16:
|
||||
version "2.0.16"
|
||||
resolved "https://registry.yarnpkg.com/cookie-universal-nuxt/-/cookie-universal-nuxt-2.0.16.tgz#8d528098c973162b352199240e40da0e5429b13f"
|
||||
integrity sha512-wRK2zw8w+a5xPehb5kLbgOic/4mbjl2exUCxWZwGuttcwsFgOymiwDrCOzmQslqrDevPDL2SsBbH6wtOm7dB9g==
|
||||
dependencies:
|
||||
"@types/cookie" "^0.3.1"
|
||||
cookie-universal "^2.0.16"
|
||||
|
||||
cookie-universal@^2.0.16:
|
||||
version "2.0.16"
|
||||
resolved "https://registry.yarnpkg.com/cookie-universal/-/cookie-universal-2.0.16.tgz#ec8b55789b502a377ef02ad230923c1dfa5c1061"
|
||||
integrity sha512-EHtQ5Tg3UoUHG7LmeV3rlV3iYthkhUuYZ0y86EseypxGcUuvzxuHExEb6mHKDhDPrIrdewAHdG/aCHuG/T4zEg==
|
||||
dependencies:
|
||||
"@types/cookie" "^0.3.1"
|
||||
cookie "^0.3.1"
|
||||
|
||||
cookie@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||
|
@ -4200,6 +4221,11 @@ jsprim@^1.2.2:
|
|||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jwt-decode@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
|
||||
integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
|
||||
|
||||
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
|
||||
|
@ -4317,11 +4343,6 @@ lodash.memoize@^4.1.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||
|
||||
lodash.merge@^4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
|
||||
integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==
|
||||
|
||||
lodash.tail@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
|
||||
|
|
|
@ -25,20 +25,19 @@ export default {
|
|||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [{ src: '@/plugins/Vuelidate.js' }],
|
||||
plugins: [{ src: '@/plugins/auth.js' }, { src: '@/plugins/vuelidate.js' }],
|
||||
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
// Doc: https://axios.nuxtjs.org/usage
|
||||
'@nuxtjs/axios'
|
||||
],
|
||||
modules: ['@nuxtjs/axios', 'cookie-universal-nuxt'],
|
||||
|
||||
/*
|
||||
** Axios module configuration
|
||||
*/
|
||||
axios: {
|
||||
// See https://github.com/nuxt-community/axios-module#options
|
||||
router: {
|
||||
middleware: 'authentication'
|
||||
},
|
||||
|
||||
env: {
|
||||
// The API base url, this will be prepended to the urls of the remote calls.
|
||||
baseUrl: 'http://localhost:8000/api/v0'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import merge from 'lodash.merge'
|
||||
import _ from 'lodash'
|
||||
import StyleLintPlugin from 'stylelint-webpack-plugin'
|
||||
|
||||
import base from './nuxt.config.base.js'
|
||||
|
@ -6,7 +6,6 @@ import base from './nuxt.config.base.js'
|
|||
const config = {
|
||||
build: {
|
||||
extend(config, ctx) {
|
||||
// Run ESLint ad Stylelint on save
|
||||
if (ctx.isDev && ctx.isClient) {
|
||||
config.module.rules.push({
|
||||
enforce: 'pre',
|
||||
|
@ -25,4 +24,4 @@ const config = {
|
|||
}
|
||||
}
|
||||
|
||||
export default merge(base, config)
|
||||
export default _.assign(base, config)
|
||||
|
|
|
@ -3,18 +3,18 @@ import path from 'path'
|
|||
import base from './nuxt.config.base.js'
|
||||
|
||||
export default function(rootDir) {
|
||||
const merge = require(rootDir + '/node_modules/lodash.merge')
|
||||
const _ = require(rootDir + '/node_modules/lodash')
|
||||
|
||||
/**
|
||||
* Because the nuxt source files are located in we web-frontend directory, but
|
||||
* the project is started from another directory we have to explicitly set the
|
||||
* source directory which contains the nuxt files and the root directory which
|
||||
* contains the node modules.
|
||||
* Because the nuxt source files are located in the web-frontend directory,
|
||||
* but the project is started from another directory we have to explicitly set
|
||||
* the source directory which contains the nuxt files and the root directory
|
||||
* which contains the node modules.
|
||||
*/
|
||||
const config = {
|
||||
rootDir: rootDir,
|
||||
srcDir: path.resolve(__dirname, '../')
|
||||
}
|
||||
|
||||
return merge(base, config)
|
||||
return _.assign(base, config)
|
||||
}
|
||||
|
|
17
web-frontend/config/nuxt.config.test.js
Normal file
17
web-frontend/config/nuxt.config.test.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { resolve } from 'path'
|
||||
import _ from 'lodash'
|
||||
|
||||
import base from './nuxt.config.base.js'
|
||||
|
||||
const config = {
|
||||
rootDir: resolve(__dirname, '../'),
|
||||
css: [],
|
||||
dev: false,
|
||||
debug: false,
|
||||
env: {
|
||||
// The API base url, this will be prepended to the urls of the remote calls.
|
||||
baseUrl: 'http://localhost/api/v0'
|
||||
}
|
||||
}
|
||||
|
||||
export default _.assign({}, base, config)
|
|
@ -1,4 +1,7 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
expand: true,
|
||||
forceExit: true,
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/$1',
|
||||
'^~/(.*)$': '<rootDir>/$1',
|
||||
|
|
13
web-frontend/middleware/authenticated.js
Normal file
13
web-frontend/middleware/authenticated.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* If this middleware is added to a page, it will redirect back to the login
|
||||
* page if the user is not authenticated.
|
||||
*/
|
||||
export default function({ req, store, redirect }) {
|
||||
// If nuxt generate, pass this middleware
|
||||
if (process.server && !req) return
|
||||
|
||||
// If the user is not authenticated we will redirect him to the login page.
|
||||
if (!store.getters['auth/isAuthenticated']) {
|
||||
redirect('/login')
|
||||
}
|
||||
}
|
16
web-frontend/middleware/authentication.js
Normal file
16
web-frontend/middleware/authentication.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export default function({ store, req, app }) {
|
||||
// If nuxt generate, pass this middleware
|
||||
if (process.server && !req) return
|
||||
|
||||
// Load the token
|
||||
const token = getToken(app.$cookies)
|
||||
|
||||
// If there already is a token we will refresh it to check if it is valid and
|
||||
// to get fresh user information. This will probably happen on the server
|
||||
// side.
|
||||
if (token && !store.getters['auth/isAuthenticated']) {
|
||||
return store.dispatch('auth/refresh', token)
|
||||
}
|
||||
}
|
|
@ -11,13 +11,15 @@
|
|||
"start": "nuxt start",
|
||||
"eslint": "eslint --ext .js,.vue .",
|
||||
"stylelint": "stylelint **/*.scss --syntax scss",
|
||||
"test": "jest"
|
||||
"test": "jest -i --verbose false test/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.8.2",
|
||||
"@nuxtjs/axios": "^5.3.6",
|
||||
"@nuxtjs/axios": "^5.5.4",
|
||||
"cookie-universal-nuxt": "^2.0.16",
|
||||
"cross-env": "^5.2.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-sass": "^4.12.0",
|
||||
"normalize-scss": "^7.0.1",
|
||||
"nuxt": "^2.4.0",
|
||||
|
@ -43,6 +45,9 @@
|
|||
"eslint-plugin-standard": ">=4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"jest": "^24.1.0",
|
||||
"jsdom": "^15.1.1",
|
||||
"moxios": "^0.4.0",
|
||||
"node-mocks-http": "^1.7.6",
|
||||
"nodemon": "^1.18.9",
|
||||
"prettier": "^1.16.4",
|
||||
"stylelint": "^9.2.1",
|
||||
|
|
18
web-frontend/pages/app/index.vue
Normal file
18
web-frontend/pages/app/index.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>Welcome {{ user }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
middleware: 'authenticated',
|
||||
computed: {
|
||||
...mapState({
|
||||
user: state => state.auth.user
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>Baserow</h1>
|
||||
</div>
|
||||
<div>{{ test }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
fetch({ store, redirect }) {
|
||||
const name = store.getters['auth/isAuthenticated'] ? 'app' : 'login'
|
||||
redirect({ name: name })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,14 +3,24 @@
|
|||
<h1 class="box-title">
|
||||
<img src="@/static/img/logo.svg" alt="" />
|
||||
</h1>
|
||||
<div v-if="invalid" class="alert alert-error alert-has-icon">
|
||||
<div class="alert-icon">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
</div>
|
||||
<div class="alert-title">Incorrect credentials</div>
|
||||
<p class="alert-content">
|
||||
The provided e-mail address or password is incorrect.
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="login">
|
||||
<div class="control">
|
||||
<label class="control-label">E-mail address</label>
|
||||
<div class="control-elements">
|
||||
<input
|
||||
ref="email"
|
||||
v-model="credentials.email"
|
||||
:class="{ 'input-error': $v.credentials.email.$error }"
|
||||
type="text"
|
||||
type="email"
|
||||
class="input input-large"
|
||||
@blur="$v.credentials.email.$touch()"
|
||||
/>
|
||||
|
@ -23,6 +33,7 @@
|
|||
<label class="control-label">Password</label>
|
||||
<div class="control-elements">
|
||||
<input
|
||||
ref="password"
|
||||
v-model="credentials.password"
|
||||
:class="{ 'input-error': $v.credentials.password.$error }"
|
||||
type="password"
|
||||
|
@ -67,6 +78,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
invalid: false,
|
||||
credentials: {
|
||||
email: '',
|
||||
password: ''
|
||||
|
@ -84,6 +96,23 @@ export default {
|
|||
this.$v.$touch()
|
||||
if (!this.$v.$invalid) {
|
||||
this.loading = true
|
||||
this.$store
|
||||
.dispatch('auth/login', {
|
||||
email: this.credentials.email,
|
||||
password: this.credentials.password
|
||||
})
|
||||
.then(() => {
|
||||
this.$nuxt.$router.replace({ name: 'app' })
|
||||
})
|
||||
.catch(() => {
|
||||
this.invalid = true
|
||||
this.credentials.password = ''
|
||||
this.$v.$reset()
|
||||
this.$refs.password.focus()
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="box-title">Sign up</h1>
|
||||
<div
|
||||
v-if="error == 'ERROR_ALREADY_EXISTS'"
|
||||
class="alert alert-error alert-has-icon"
|
||||
>
|
||||
<div class="alert-icon">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
</div>
|
||||
<div class="alert-title">User already exists</div>
|
||||
<p class="alert-content">
|
||||
A user with the provided e-mail address already exists.
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="register">
|
||||
<div class="control">
|
||||
<label class="control-label">E-mail address</label>
|
||||
|
@ -108,6 +120,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
error: '',
|
||||
loading: false,
|
||||
account: {
|
||||
email: '',
|
||||
|
@ -122,6 +135,23 @@ export default {
|
|||
this.$v.$touch()
|
||||
if (!this.$v.$invalid) {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
this.$store
|
||||
.dispatch('auth/register', {
|
||||
name: this.account.name,
|
||||
email: this.account.email,
|
||||
password: this.account.password
|
||||
})
|
||||
.then(() => {
|
||||
this.$nuxt.$router.replace({ name: 'app' })
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error.responseError
|
||||
this.$v.$reset()
|
||||
})
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
24
web-frontend/plugins/auth.js
Normal file
24
web-frontend/plugins/auth.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { client } from '@/services/client'
|
||||
|
||||
export default function({ store }) {
|
||||
// Create a request interceptor to add the authorization token to every
|
||||
// request if the user is authenticated.
|
||||
client.interceptors.request.use(config => {
|
||||
if (store.getters['auth/isAuthenticated']) {
|
||||
const token = store.getters['auth/token']
|
||||
config.headers.Authorization = `JWT: ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// If the user is authenticated, but is not refreshing in the browser means
|
||||
// that the refresh was done on the server side, so we need to manually start
|
||||
// the refreshing timeout here.
|
||||
if (
|
||||
store.getters['auth/isAuthenticated'] &&
|
||||
!store.getters['auth/isRefreshing'] &&
|
||||
process.browser
|
||||
) {
|
||||
store.dispatch('auth/startRefreshTimeout')
|
||||
}
|
||||
}
|
23
web-frontend/services/auth.js
Normal file
23
web-frontend/services/auth.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { client } from './client'
|
||||
|
||||
export default {
|
||||
login(username, password) {
|
||||
return client.post('/user/token-auth/', {
|
||||
username,
|
||||
password
|
||||
})
|
||||
},
|
||||
refresh(token) {
|
||||
return client.post('/user/token-refresh/', {
|
||||
token
|
||||
})
|
||||
},
|
||||
register(email, name, password, authenticate = true) {
|
||||
return client.post('/user/', {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
authenticate
|
||||
})
|
||||
}
|
||||
}
|
31
web-frontend/services/client.js
Normal file
31
web-frontend/services/client.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export const client = axios.create({
|
||||
baseURL: process.env.baseUrl,
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
client.interceptors.response.use(
|
||||
response => {
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
error.responseError = undefined
|
||||
error.responseDetail = undefined
|
||||
|
||||
if (
|
||||
error.response &&
|
||||
'error' in error.response.data &&
|
||||
'detail' in error.response.data
|
||||
) {
|
||||
error.responseError = error.response.data.error
|
||||
error.responseDetail = error.response.data.detail
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
112
web-frontend/store/auth.js
Normal file
112
web-frontend/store/auth.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
import AuthService from '@/services/auth'
|
||||
import { setToken, unsetToken } from '@/utils/auth'
|
||||
|
||||
export const state = () => ({
|
||||
refreshing: false,
|
||||
token: null,
|
||||
user: null
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_USER_DATA(state, { token, user }) {
|
||||
state.token = token
|
||||
state.token_data = jwtDecode(token)
|
||||
state.user = user
|
||||
},
|
||||
CLEAR_USER_DATA(state) {
|
||||
state.token = null
|
||||
state.user = null
|
||||
},
|
||||
SET_REFRESHING(state, refreshing) {
|
||||
state.refreshing = refreshing
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
/**
|
||||
* Authenticate a user by his email and password. If successful commit the
|
||||
* token to the state and start the refresh timeout to stay authenticated.
|
||||
*/
|
||||
login({ commit, dispatch }, { email, password }) {
|
||||
return AuthService.login(email, password).then(({ data }) => {
|
||||
setToken(data.token, this.app.$cookies)
|
||||
commit('SET_USER_DATA', data)
|
||||
dispatch('startRefreshTimeout')
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Register a new user and immediately authenticate. If successful commit the
|
||||
* token to the state and start the refresh timeout to stay authenticated.
|
||||
*/
|
||||
register({ commit, dispatch }, { email, name, password }) {
|
||||
return AuthService.register(email, name, password, true).then(
|
||||
({ data }) => {
|
||||
setToken(data.token, this.app.$cookies)
|
||||
commit('SET_USER_DATA', data)
|
||||
dispatch('startRefreshTimeout')
|
||||
}
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Refresh the existing token. If successful commit the new token and start a
|
||||
* new refresh timeout. If unsuccessful the existing cookie and user data is
|
||||
* cleared.
|
||||
*/
|
||||
refresh({ commit, state, dispatch }, token) {
|
||||
return AuthService.refresh(token)
|
||||
.then(({ data }) => {
|
||||
setToken(data.token, this.app.$cookies)
|
||||
commit('SET_USER_DATA', data)
|
||||
dispatch('startRefreshTimeout')
|
||||
})
|
||||
.catch(() => {
|
||||
// The token could not be refreshed, this means the token is no longer
|
||||
// valid and the user not logged in anymore.
|
||||
unsetToken(this.app.$cookies)
|
||||
commit('CLEAR_USER_DATA')
|
||||
|
||||
// @TODO we might want to do something here, trigger some event, show
|
||||
// show the user a login popup or redirect to the login page.
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Because the token expires within a configurable time, we need to keep
|
||||
* refreshing the token before that happens. This process may only happen in
|
||||
* the browser because that is where we measure if the user still has the
|
||||
* application open.
|
||||
*/
|
||||
startRefreshTimeout({ getters, commit, dispatch }) {
|
||||
if (!process.browser) return
|
||||
|
||||
clearTimeout(this.refreshTimeout)
|
||||
commit('SET_REFRESHING', true)
|
||||
|
||||
this.refreshTimeout = setTimeout(() => {
|
||||
dispatch('refresh', getters.token)
|
||||
commit('SET_REFRESHING', false)
|
||||
}, (getters.tokenExpireSeconds - 10) * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
isAuthenticated(state) {
|
||||
return !!state.user
|
||||
},
|
||||
isRefreshing(state) {
|
||||
return state.refresh
|
||||
},
|
||||
token(state) {
|
||||
return state.token
|
||||
},
|
||||
/**
|
||||
* Returns the amount of seconds it will take before the tokes expires.
|
||||
* @TODO figure out what happens if the browser and server time and not very
|
||||
* much in sync.
|
||||
*/
|
||||
tokenExpireSeconds(state) {
|
||||
const now = Math.ceil(new Date().getTime() / 1000)
|
||||
return state.token_data.exp - now
|
||||
}
|
||||
}
|
20
web-frontend/test/config/config.spec.js
Normal file
20
web-frontend/test/config/config.spec.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import createNuxt from '@/test/helpers/create-nuxt'
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('Config', () => {
|
||||
beforeAll(async done => {
|
||||
nuxt = await createNuxt(true)
|
||||
done()
|
||||
}, 120000)
|
||||
|
||||
test('Plugins', () => {
|
||||
const plugins = nuxt.options.plugins.map(option => option.src)
|
||||
expect(plugins.includes('@/plugins/auth.js')).toBe(true)
|
||||
expect(plugins.includes('@/plugins/vuelidate.js')).toBe(true)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
})
|
13
web-frontend/test/helpers/create-nuxt.js
Normal file
13
web-frontend/test/helpers/create-nuxt.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Builder, Nuxt } from 'nuxt'
|
||||
|
||||
import config from '@/config/nuxt.config.test'
|
||||
|
||||
export default async function createNuxt(buildAndListen = false) {
|
||||
const nuxt = new Nuxt(config)
|
||||
await nuxt.ready()
|
||||
if (buildAndListen) {
|
||||
await new Builder(nuxt).build()
|
||||
await nuxt.server.listen(3001, 'localhost')
|
||||
}
|
||||
return nuxt
|
||||
}
|
|
@ -1,9 +1,56 @@
|
|||
import { mount } from '@vue/test-utils'
|
||||
import Index from '@/pages/index.vue'
|
||||
import moxios from 'moxios'
|
||||
import httpMocks from 'node-mocks-http'
|
||||
|
||||
describe('Home', () => {
|
||||
test('is a Vue instance', () => {
|
||||
const wrapper = mount(Index)
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
import createNuxt from '@/test/helpers/create-nuxt'
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('index redirect', () => {
|
||||
beforeAll(async done => {
|
||||
moxios.install()
|
||||
|
||||
// Because the token 'test1' exists it will be refreshed immediately, the
|
||||
// refresh endpoint is stubbed so that it will always provide a valid
|
||||
// unexpired token.
|
||||
moxios.stubRequest('http://localhost/api/v0/user/token-refresh/', {
|
||||
status: 200,
|
||||
response: {
|
||||
token:
|
||||
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2' +
|
||||
'VybmFtZSI6InRlc3RAdGVzdCIsImV4cCI6MTk5OTk5OTk5OSwiZW1haWwiO' +
|
||||
'iJ0ZXN0QHRlc3QubmwiLCJvcmlnX2lhdCI6MTU2Mjc3MzQxNH0.2i0gqrcH' +
|
||||
'5uy7mk4kf3LoLpZYXoyMrOfi0fDQneVcaFE',
|
||||
user: {
|
||||
first_name: 'Test',
|
||||
username: 'test@test.nl'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
nuxt = await createNuxt(true)
|
||||
done()
|
||||
}, 120000)
|
||||
|
||||
test('if not authenticated', async () => {
|
||||
const { redirected } = await nuxt.server.renderRoute('/')
|
||||
expect(redirected.path).toBe('/login')
|
||||
expect(redirected.status).toBe(302)
|
||||
})
|
||||
|
||||
test('if authenticated', async () => {
|
||||
const req = httpMocks.createRequest({
|
||||
headers: {
|
||||
cookie: 'jwt_token=test1'
|
||||
}
|
||||
})
|
||||
const res = httpMocks.createResponse()
|
||||
const { redirected } = await nuxt.server.renderRoute('/', { req, res })
|
||||
expect(redirected.path).toBe('/app')
|
||||
expect(redirected.status).toBe(302)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
moxios.uninstall()
|
||||
})
|
||||
})
|
||||
|
|
19
web-frontend/test/pages/login/index.spec.js
Normal file
19
web-frontend/test/pages/login/index.spec.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import createNuxt from '@/test/helpers/create-nuxt'
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('children', () => {
|
||||
beforeAll(async done => {
|
||||
nuxt = await createNuxt(true)
|
||||
done()
|
||||
}, 120000)
|
||||
|
||||
test('/login', async () => {
|
||||
const { html } = await nuxt.server.renderRoute('/login')
|
||||
expect(html).toContain('Login')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
})
|
19
web-frontend/test/pages/login/signup.spec.js
Normal file
19
web-frontend/test/pages/login/signup.spec.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import createNuxt from '@/test/helpers/create-nuxt'
|
||||
|
||||
let nuxt = null
|
||||
|
||||
describe('children', () => {
|
||||
beforeAll(async done => {
|
||||
nuxt = await createNuxt(true)
|
||||
done()
|
||||
}, 120000)
|
||||
|
||||
test('/login', async () => {
|
||||
const { html } = await nuxt.server.renderRoute('/login/signup')
|
||||
expect(html).toContain('Sign up')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nuxt.close()
|
||||
})
|
||||
})
|
15
web-frontend/utils/auth.js
Normal file
15
web-frontend/utils/auth.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
const cookieTokenName = 'jwt_token'
|
||||
|
||||
export const setToken = (token, cookie) => {
|
||||
if (process.SERVER_BUILD) return
|
||||
cookie.set(cookieTokenName, token)
|
||||
}
|
||||
|
||||
export const unsetToken = cookie => {
|
||||
if (process.SERVER_BUILD) return
|
||||
cookie.remove(cookieTokenName)
|
||||
}
|
||||
|
||||
export const getToken = cookie => {
|
||||
return cookie.get(cookieTokenName)
|
||||
}
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue