mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-10 23:50:12 +00:00
Merge branch '187-disable-signup' into 'develop'
Resolve "Disable Signup" Closes #187 See merge request bramw/baserow!173
This commit is contained in:
commit
78ef2c2942
43 changed files with 854 additions and 134 deletions
backend
src/baserow
api
config/settings
core
ws
tests
baserow
api
core
fixtures
web-frontend/modules/core
adminTypes.js
assets/scss/components
components/sidebar
layouts
middleware.jsmiddleware
pages
plugin.jsroutes.jsservices
store
0
backend/src/baserow/api/settings/__init__.py
Normal file
0
backend/src/baserow/api/settings/__init__.py
Normal file
12
backend/src/baserow/api/settings/serializers.py
Normal file
12
backend/src/baserow/api/settings/serializers.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from baserow.core.models import Settings
|
||||
|
||||
|
||||
class SettingsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Settings
|
||||
fields = ('allow_new_signups',)
|
||||
extra_kwargs = {
|
||||
'allow_new_signups': {'required': False},
|
||||
}
|
11
backend/src/baserow/api/settings/urls.py
Normal file
11
backend/src/baserow/api/settings/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from .views import SettingsView, UpdateSettingsView
|
||||
|
||||
|
||||
app_name = 'baserow.api.settings'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^update/$', UpdateSettingsView.as_view(), name='update'),
|
||||
url(r'^$', SettingsView.as_view(), name='get'),
|
||||
]
|
58
backend/src/baserow/api/settings/views.py
Normal file
58
backend/src/baserow/api/settings/views.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from django.db import transaction
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser
|
||||
|
||||
from baserow.api.decorators import validate_body
|
||||
from baserow.core.handler import CoreHandler
|
||||
|
||||
from .serializers import SettingsSerializer
|
||||
|
||||
|
||||
class SettingsView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@extend_schema(
|
||||
tags=['Settings'],
|
||||
operation_id='get_settings',
|
||||
description='Responds with all the admin configured settings.',
|
||||
responses={
|
||||
200: SettingsSerializer,
|
||||
},
|
||||
auth=[None],
|
||||
)
|
||||
def get(self, request):
|
||||
"""
|
||||
Responds with all the admin configured settings.
|
||||
"""
|
||||
|
||||
settings = CoreHandler().get_settings()
|
||||
return Response(SettingsSerializer(settings).data)
|
||||
|
||||
|
||||
class UpdateSettingsView(APIView):
|
||||
permission_classes = (IsAdminUser,)
|
||||
|
||||
@extend_schema(
|
||||
tags=['Settings'],
|
||||
operation_id='update_settings',
|
||||
description=(
|
||||
'Updates the admin configured settings if the user has admin permissions.'
|
||||
),
|
||||
request=SettingsSerializer,
|
||||
responses={
|
||||
200: SettingsSerializer,
|
||||
},
|
||||
)
|
||||
@validate_body(SettingsSerializer)
|
||||
@transaction.atomic
|
||||
def patch(self, request, data):
|
||||
"""
|
||||
Updates the provided config settings if the user has admin permissions.
|
||||
"""
|
||||
|
||||
settings = CoreHandler().update_settings(request.user, **data)
|
||||
return Response(SettingsSerializer(settings).data)
|
|
@ -4,6 +4,7 @@ from drf_spectacular.views import SpectacularJSONAPIView, SpectacularRedocView
|
|||
|
||||
from baserow.core.registries import plugin_registry, application_type_registry
|
||||
|
||||
from .settings import urls as settings_urls
|
||||
from .user import urls as user_urls
|
||||
from .user_files import urls as user_files_urls
|
||||
from .groups import urls as group_urls
|
||||
|
@ -19,6 +20,7 @@ urlpatterns = [
|
|||
SpectacularRedocView.as_view(url_name='api:json_schema'),
|
||||
name='redoc'
|
||||
),
|
||||
path('settings/', include(settings_urls, namespace='settings')),
|
||||
path('user/', include(user_urls, namespace='user')),
|
||||
path('user-files/', include(user_files_urls, namespace='user_files')),
|
||||
path('groups/', include(group_urls, namespace='groups')),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
ERROR_ALREADY_EXISTS = 'ERROR_EMAIL_ALREADY_EXISTS'
|
||||
ERROR_USER_NOT_FOUND = 'ERROR_USER_NOT_FOUND'
|
||||
ERROR_INVALID_OLD_PASSWORD = 'ERROR_INVALID_OLD_PASSWORD'
|
||||
ERROR_DISABLED_SIGNUP = 'ERROR_DISABLED_SIGNUP'
|
||||
|
|
|
@ -13,11 +13,10 @@ User = get_user_model()
|
|||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'username', 'password')
|
||||
fields = ('first_name', 'username', 'password', 'is_staff')
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
'write_only': True
|
||||
}
|
||||
'password': {'write_only': True},
|
||||
'is_staff': {'read_only': True},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ from baserow.core.exceptions import (
|
|||
from baserow.core.models import GroupInvitation
|
||||
from baserow.core.user.handler import UserHandler
|
||||
from baserow.core.user.exceptions import (
|
||||
UserAlreadyExist, UserNotFound, InvalidPassword
|
||||
UserAlreadyExist, UserNotFound, InvalidPassword, DisabledSignupError
|
||||
)
|
||||
|
||||
from .serializers import (
|
||||
|
@ -39,7 +39,8 @@ from .serializers import (
|
|||
NormalizedEmailWebTokenSerializer, DashboardSerializer
|
||||
)
|
||||
from .errors import (
|
||||
ERROR_ALREADY_EXISTS, ERROR_USER_NOT_FOUND, ERROR_INVALID_OLD_PASSWORD
|
||||
ERROR_ALREADY_EXISTS, ERROR_USER_NOT_FOUND, ERROR_INVALID_OLD_PASSWORD,
|
||||
ERROR_DISABLED_SIGNUP
|
||||
)
|
||||
from .schemas import create_user_response_schema, authenticate_user_schema
|
||||
|
||||
|
@ -146,7 +147,8 @@ class UserView(APIView):
|
|||
UserAlreadyExist: ERROR_ALREADY_EXISTS,
|
||||
BadSignature: BAD_TOKEN_SIGNATURE,
|
||||
GroupInvitationDoesNotExist: ERROR_GROUP_INVITATION_DOES_NOT_EXIST,
|
||||
GroupInvitationEmailMismatch: ERROR_GROUP_INVITATION_EMAIL_MISMATCH
|
||||
GroupInvitationEmailMismatch: ERROR_GROUP_INVITATION_EMAIL_MISMATCH,
|
||||
DisabledSignupError: ERROR_DISABLED_SIGNUP
|
||||
})
|
||||
@validate_body(RegisterSerializer)
|
||||
def post(self, request, data):
|
||||
|
|
|
@ -185,6 +185,7 @@ SPECTACULAR_SETTINGS = {
|
|||
'VERSION': '0.8.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'TAGS': [
|
||||
{'name': 'Settings'},
|
||||
{'name': 'User'},
|
||||
{'name': 'User files'},
|
||||
{'name': 'Groups'},
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
class IsNotAdminError(Exception):
|
||||
"""
|
||||
Raised when the user tries to perform an action that is not allowed because he
|
||||
does not have admin permissions.
|
||||
"""
|
||||
|
||||
|
||||
class UserNotInGroupError(Exception):
|
||||
"""Raised when the user doesn't have access to the related group."""
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ from django.conf import settings
|
|||
from baserow.core.user.utils import normalize_email_address
|
||||
|
||||
from .models import (
|
||||
Group, GroupUser, GroupInvitation, Application, GROUP_USER_PERMISSION_CHOICES,
|
||||
GROUP_USER_PERMISSION_ADMIN
|
||||
Settings, Group, GroupUser, GroupInvitation, Application,
|
||||
GROUP_USER_PERMISSION_CHOICES, GROUP_USER_PERMISSION_ADMIN
|
||||
)
|
||||
from .exceptions import (
|
||||
GroupDoesNotExist, ApplicationDoesNotExist, BaseURLHostnameNotAllowed,
|
||||
GroupInvitationEmailMismatch, GroupInvitationDoesNotExist, GroupUserDoesNotExist,
|
||||
GroupUserAlreadyExists
|
||||
GroupUserAlreadyExists, IsNotAdminError
|
||||
)
|
||||
from .utils import extract_allowed, set_allowed_attrs
|
||||
from .registries import application_type_registry
|
||||
|
@ -24,6 +24,46 @@ from .emails import GroupInvitationEmail
|
|||
|
||||
|
||||
class CoreHandler:
|
||||
def get_settings(self):
|
||||
"""
|
||||
Returns a settings model instance containing all the admin configured settings.
|
||||
|
||||
:return: The settings instance.
|
||||
:rtype: Settings
|
||||
"""
|
||||
|
||||
try:
|
||||
return Settings.objects.all()[:1].get()
|
||||
except Settings.DoesNotExist:
|
||||
return Settings.objects.create()
|
||||
|
||||
def update_settings(self, user, settings_instance=None, **kwargs):
|
||||
"""
|
||||
Updates one or more setting values if the user has staff permissions.
|
||||
|
||||
:param user: The user on whose behalf the settings are updated.
|
||||
:type user: User
|
||||
:param settings_instance: If already fetched, the settings instance can be
|
||||
provided to avoid fetching the values for a second time.
|
||||
:type settings_instance: Settings
|
||||
:param kwargs: An dict containing the settings that need to be updated.
|
||||
:type kwargs: dict
|
||||
:return: The update settings instance.
|
||||
:rtype: Settings
|
||||
"""
|
||||
|
||||
if not user.is_staff:
|
||||
raise IsNotAdminError(user)
|
||||
|
||||
if not settings_instance:
|
||||
settings_instance = self.get_settings()
|
||||
|
||||
for name, value in kwargs.items():
|
||||
setattr(settings_instance, name, value)
|
||||
|
||||
settings_instance.save()
|
||||
return settings_instance
|
||||
|
||||
def get_group(self, group_id, base_queryset=None):
|
||||
"""
|
||||
Selects a group with a given id from the database.
|
||||
|
|
29
backend/src/baserow/core/migrations/0005_settings.py
Normal file
29
backend/src/baserow/core/migrations/0005_settings.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 2.2.11 on 2021-02-15 13:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_auto_20210126_1950'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Settings',
|
||||
fields=[
|
||||
('id', models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID'
|
||||
)),
|
||||
('allow_new_signups', models.BooleanField(
|
||||
default=True,
|
||||
help_text='Indicates whether new users can create a new account '
|
||||
'when signing up.'
|
||||
)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -31,6 +31,19 @@ def get_default_application_content_type():
|
|||
return ContentType.objects.get_for_model(Application)
|
||||
|
||||
|
||||
class Settings(models.Model):
|
||||
"""
|
||||
The settings model represents the application wide settings that only admins can
|
||||
change. This table can only contain a single row.
|
||||
"""
|
||||
|
||||
allow_new_signups = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Indicates whether new users can create a new account when signing '
|
||||
'up.'
|
||||
)
|
||||
|
||||
|
||||
class Group(CreatedAndUpdatedOnMixin, models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
users = models.ManyToManyField(User, through='GroupUser')
|
||||
|
|
|
@ -8,3 +8,9 @@ class UserAlreadyExist(Exception):
|
|||
|
||||
class InvalidPassword(Exception):
|
||||
"""Raised when the provided password is incorrect."""
|
||||
|
||||
|
||||
class DisabledSignupError(Exception):
|
||||
"""
|
||||
Raised when a user account is created when the new signup setting is disabled.
|
||||
"""
|
||||
|
|
|
@ -12,7 +12,9 @@ from baserow.core.exceptions import (
|
|||
)
|
||||
from baserow.core.exceptions import GroupInvitationEmailMismatch
|
||||
|
||||
from .exceptions import UserAlreadyExist, UserNotFound, InvalidPassword
|
||||
from .exceptions import (
|
||||
UserAlreadyExist, UserNotFound, InvalidPassword, DisabledSignupError
|
||||
)
|
||||
from .emails import ResetPasswordEmail
|
||||
from .utils import normalize_email_address
|
||||
|
||||
|
@ -72,10 +74,14 @@ class UserHandler:
|
|||
already exists.
|
||||
:raises GroupInvitationEmailMismatch: If the group invitation email does not
|
||||
match the one of the user.
|
||||
:raises SignupDisabledError: If signing up is disabled.
|
||||
:return: The user object.
|
||||
:rtype: User
|
||||
"""
|
||||
|
||||
if not CoreHandler().get_settings().allow_new_signups:
|
||||
raise DisabledSignupError('Sign up is disabled.')
|
||||
|
||||
email = normalize_email_address(email)
|
||||
|
||||
if User.objects.filter(Q(email=email) | Q(username=email)).exists():
|
||||
|
|
|
@ -139,5 +139,5 @@ class CoreConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.send_json(payload)
|
||||
|
||||
async def disconnect(self, message):
|
||||
self.discard_current_page()
|
||||
await self.discard_current_page()
|
||||
await self.channel_layer.group_discard('users', self.channel_name)
|
||||
|
|
75
backend/tests/baserow/api/settings/test_settings_views.py
Normal file
75
backend/tests/baserow/api/settings/test_settings_views.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import pytest
|
||||
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from baserow.core.models import Settings
|
||||
from baserow.core.handler import CoreHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_settings(api_client):
|
||||
response = api_client.get(reverse('api:settings:get'))
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert response_json['allow_new_signups'] is True
|
||||
|
||||
settings = Settings.objects.first()
|
||||
settings.allow_new_signups = False
|
||||
settings.save()
|
||||
|
||||
response = api_client.get(reverse('api:settings:get'))
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert response_json['allow_new_signups'] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_settings(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(is_staff=True)
|
||||
user_2, token_2 = data_fixture.create_user_and_token()
|
||||
|
||||
response = api_client.patch(
|
||||
reverse('api:settings:update'),
|
||||
{'allow_new_signups': False},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token_2}'
|
||||
)
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
assert CoreHandler().get_settings().allow_new_signups is True
|
||||
|
||||
response = api_client.patch(
|
||||
reverse('api:settings:update'),
|
||||
{'allow_new_signups': {}},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
response_json = response.json()
|
||||
assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
|
||||
assert response_json['detail']['allow_new_signups'][0]['code'] == 'invalid'
|
||||
|
||||
response = api_client.patch(
|
||||
reverse('api:settings:update'),
|
||||
{'allow_new_signups': False},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert response_json['allow_new_signups'] is False
|
||||
assert CoreHandler().get_settings().allow_new_signups is False
|
||||
|
||||
response = api_client.patch(
|
||||
reverse('api:settings:update'),
|
||||
{},
|
||||
format='json',
|
||||
HTTP_AUTHORIZATION=f'JWT {token}'
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert response_json['allow_new_signups'] is False
|
||||
assert CoreHandler().get_settings().allow_new_signups is False
|
|
@ -53,6 +53,7 @@ def test_token_auth(api_client, data_fixture):
|
|||
assert 'user' in json
|
||||
assert json['user']['username'] == 'test@test.nl'
|
||||
assert json['user']['first_name'] == 'Test1'
|
||||
assert json['user']['is_staff'] is False
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.last_login == datetime(2020, 1, 1, 12, 00, tzinfo=timezone('UTC'))
|
||||
|
@ -68,6 +69,7 @@ def test_token_auth(api_client, data_fixture):
|
|||
assert 'user' in json
|
||||
assert json['user']['username'] == 'test@test.nl'
|
||||
assert json['user']['first_name'] == 'Test1'
|
||||
assert json['user']['is_staff'] is False
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.last_login == datetime(2020, 1, 2, 12, 00, tzinfo=timezone('UTC'))
|
||||
|
@ -85,7 +87,12 @@ def test_token_refresh(api_client, data_fixture):
|
|||
response = api_client.post(reverse('api:user:token_refresh'),
|
||||
{'token': token}, format='json')
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert 'token' in response.json()
|
||||
json = response.json()
|
||||
assert 'token' in json
|
||||
assert 'user' in json
|
||||
assert json['user']['username'] == 'test@test.nl'
|
||||
assert json['user']['first_name'] == 'Test1'
|
||||
assert json['user']['is_staff'] is False
|
||||
|
||||
with patch('rest_framework_jwt.utils.datetime') as mock_datetime:
|
||||
mock_datetime.utcnow.return_value = datetime(2019, 1, 1, 1, 1, 1, 0)
|
||||
|
|
|
@ -16,7 +16,7 @@ User = get_user_model()
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_user(client):
|
||||
def test_create_user(client, data_fixture):
|
||||
response = client.post(reverse('api:user:index'), {
|
||||
'name': 'Test1',
|
||||
'email': 'test@test.nl',
|
||||
|
@ -31,6 +31,7 @@ def test_create_user(client):
|
|||
assert 'password' not in response_json['user']
|
||||
assert response_json['user']['username'] == 'test@test.nl'
|
||||
assert response_json['user']['first_name'] == 'Test1'
|
||||
assert response_json['user']['is_staff'] is False
|
||||
|
||||
response_failed = client.post(reverse('api:user:index'), {
|
||||
'name': 'Test1',
|
||||
|
@ -48,6 +49,16 @@ def test_create_user(client):
|
|||
assert response_failed.status_code == 400
|
||||
assert response_failed.json()['error'] == 'ERROR_EMAIL_ALREADY_EXISTS'
|
||||
|
||||
data_fixture.update_settings(allow_new_signups=False)
|
||||
response_failed = client.post(reverse('api:user:index'), {
|
||||
'name': 'Test1',
|
||||
'email': 'test10@test.nl',
|
||||
'password': 'test12'
|
||||
}, format='json')
|
||||
assert response_failed.status_code == 400
|
||||
assert response_failed.json()['error'] == 'ERROR_DISABLED_SIGNUP'
|
||||
data_fixture.update_settings(allow_new_signups=True)
|
||||
|
||||
response_failed_2 = client.post(reverse('api:user:index'), {
|
||||
'email': 'test'
|
||||
}, format='json')
|
||||
|
|
|
@ -7,17 +7,40 @@ from django.db import connection
|
|||
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import (
|
||||
Group, GroupUser, GroupInvitation, Application, GROUP_USER_PERMISSION_ADMIN
|
||||
Settings, Group, GroupUser, GroupInvitation, Application,
|
||||
GROUP_USER_PERMISSION_ADMIN
|
||||
)
|
||||
from baserow.core.exceptions import (
|
||||
UserNotInGroupError, ApplicationTypeDoesNotExist, GroupDoesNotExist,
|
||||
GroupUserDoesNotExist, ApplicationDoesNotExist, UserInvalidGroupPermissionsError,
|
||||
BaseURLHostnameNotAllowed, GroupInvitationEmailMismatch,
|
||||
GroupInvitationDoesNotExist, GroupUserAlreadyExists
|
||||
GroupInvitationDoesNotExist, GroupUserAlreadyExists, IsNotAdminError
|
||||
)
|
||||
from baserow.contrib.database.models import Database, Table
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_settings():
|
||||
settings = CoreHandler().get_settings()
|
||||
assert isinstance(settings, Settings)
|
||||
assert settings.allow_new_signups is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_settings(data_fixture):
|
||||
user_1 = data_fixture.create_user(is_staff=True)
|
||||
user_2 = data_fixture.create_user()
|
||||
|
||||
with pytest.raises(IsNotAdminError):
|
||||
CoreHandler().update_settings(user_2, allow_new_signups=False)
|
||||
|
||||
settings = CoreHandler().update_settings(user_1, allow_new_signups=False)
|
||||
assert settings.allow_new_signups is False
|
||||
|
||||
settings = Settings.objects.all().first()
|
||||
assert settings.allow_new_signups is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_group(data_fixture):
|
||||
user_1 = data_fixture.create_user()
|
||||
|
|
|
@ -5,6 +5,8 @@ from freezegun import freeze_time
|
|||
|
||||
from itsdangerous.exc import SignatureExpired, BadSignature
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from baserow.core.models import Group, GroupUser
|
||||
from baserow.core.registries import plugin_registry
|
||||
from baserow.contrib.database.models import (
|
||||
|
@ -17,11 +19,14 @@ from baserow.core.exceptions import (
|
|||
)
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.user.exceptions import (
|
||||
UserAlreadyExist, UserNotFound, InvalidPassword
|
||||
UserAlreadyExist, UserNotFound, InvalidPassword, DisabledSignupError
|
||||
)
|
||||
from baserow.core.user.handler import UserHandler
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_user(data_fixture):
|
||||
user_1 = data_fixture.create_user(email='user1@localhost')
|
||||
|
@ -42,12 +47,18 @@ def test_get_user(data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_user():
|
||||
def test_create_user(data_fixture):
|
||||
plugin_mock = MagicMock()
|
||||
plugin_registry.registry['mock'] = plugin_mock
|
||||
|
||||
user_handler = UserHandler()
|
||||
|
||||
data_fixture.update_settings(allow_new_signups=False)
|
||||
with pytest.raises(DisabledSignupError):
|
||||
user_handler.create_user('Test1', 'test@test.nl', 'password')
|
||||
assert User.objects.all().count() == 0
|
||||
data_fixture.update_settings(allow_new_signups=True)
|
||||
|
||||
user = user_handler.create_user('Test1', 'test@test.nl', 'password')
|
||||
assert user.pk
|
||||
assert user.first_name == 'Test1'
|
||||
|
|
6
backend/tests/fixtures/__init__.py
vendored
6
backend/tests/fixtures/__init__.py
vendored
|
@ -1,5 +1,6 @@
|
|||
from faker import Faker
|
||||
|
||||
from .settings import SettingsFixtures
|
||||
from .user import UserFixtures
|
||||
from .user_file import UserFileFixtures
|
||||
from .group import GroupFixtures
|
||||
|
@ -10,6 +11,7 @@ from .field import FieldFixtures
|
|||
from .token import TokenFixtures
|
||||
|
||||
|
||||
class Fixtures(UserFixtures, UserFileFixtures, GroupFixtures, ApplicationFixtures,
|
||||
TableFixtures, ViewFixtures, FieldFixtures, TokenFixtures):
|
||||
class Fixtures(SettingsFixtures, UserFixtures, UserFileFixtures, GroupFixtures,
|
||||
ApplicationFixtures, TableFixtures, ViewFixtures, FieldFixtures,
|
||||
TokenFixtures):
|
||||
fake = Faker()
|
||||
|
|
7
backend/tests/fixtures/settings.py
vendored
Normal file
7
backend/tests/fixtures/settings.py
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
from baserow.core.models import Settings
|
||||
|
||||
|
||||
class SettingsFixtures:
|
||||
def update_settings(self, **kwargs):
|
||||
settings, created = Settings.objects.update_or_create(defaults=kwargs)
|
||||
return settings
|
|
@ -21,6 +21,7 @@
|
|||
* Made it possible to configure SMTP settings via environment variables.
|
||||
* Added field name to the public REST API docs.
|
||||
* Made the public REST API docs compatible with smaller screens.
|
||||
* Made it possible for the admin to disable new signups.
|
||||
* Reduced the amount of queries when using the link row field.
|
||||
|
||||
## Released (2021-02-04)
|
||||
|
|
78
web-frontend/modules/core/adminTypes.js
Normal file
78
web-frontend/modules/core/adminTypes.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { Registerable } from '@baserow/modules/core/registry'
|
||||
|
||||
/**
|
||||
* An admin type is visible in the sidebar under the admin menu item. All
|
||||
* registered admin types are visible in the sidebar to admins and he clicks
|
||||
* on one he is redirected to the route related to the admin type.
|
||||
*/
|
||||
export class AdminType extends Registerable {
|
||||
/**
|
||||
* The font awesome 5 icon name that is used as convenience for the user to
|
||||
* recognize admin types. The icon will for example be displayed in the
|
||||
* sidebar. If you for example want the database icon, you must return
|
||||
* 'database' here. This will result in the classname 'fas fa-database'.
|
||||
*/
|
||||
getIconClass() {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A human readable name of the admin type. This will be shown in the sidebar
|
||||
* if the user is an admin.
|
||||
*/
|
||||
getName() {
|
||||
return null
|
||||
}
|
||||
|
||||
getRouteName() {
|
||||
throw new Error('The route name of an admin type must be set.')
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.type = this.getType()
|
||||
this.iconClass = this.getIconClass()
|
||||
this.name = this.getName()
|
||||
this.routeName = this.getRouteName()
|
||||
|
||||
if (this.type === null) {
|
||||
throw new Error('The type name of an admin type must be set.')
|
||||
}
|
||||
if (this.iconClass === null) {
|
||||
throw new Error('The icon class of an admin type must be set.')
|
||||
}
|
||||
if (this.name === null) {
|
||||
throw new Error('The name of an admin type must be set.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object
|
||||
*/
|
||||
serialize() {
|
||||
return {
|
||||
type: this.type,
|
||||
iconClass: this.iconClass,
|
||||
name: this.name,
|
||||
routeName: this.routeName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingsAdminType extends AdminType {
|
||||
static getType() {
|
||||
return 'settings'
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return 'cogs'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'Settings'
|
||||
}
|
||||
|
||||
getRouteName() {
|
||||
return 'admin-settings'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
.admin-settings {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.admin-settings__group {
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: solid 1px $color-neutral-200;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-settings__group-title {
|
||||
font-size: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.admin-settings__item {
|
||||
display: flex;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-settings__label {
|
||||
flex: 0 0 25%;
|
||||
max-width: 340px;
|
||||
min-width: 200px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.admin-settings__name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-settings__description {
|
||||
font-size: 13px;
|
||||
color: $color-neutral-600;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
.admin-settings__control {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
|
@ -60,3 +60,4 @@
|
|||
@import 'group_member';
|
||||
@import 'separator';
|
||||
@import 'quote';
|
||||
@import 'admin_settings';
|
||||
|
|
|
@ -147,6 +147,10 @@
|
|||
|
||||
.layout--collapsed {
|
||||
// Some minor changes regarding the tree items within the collapsed sidebar.
|
||||
.tree .sidebar__tree {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sidebar__action {
|
||||
.tree__link {
|
||||
text-align: center;
|
||||
|
@ -157,7 +161,6 @@
|
|||
}
|
||||
|
||||
.sidebar__item-name {
|
||||
margin-top: -10.5px;
|
||||
background-color: $color-neutral-900;
|
||||
color: $white;
|
||||
border-radius: 3px;
|
||||
|
@ -166,7 +169,7 @@
|
|||
font-weight: 400;
|
||||
display: none;
|
||||
|
||||
@include absolute(50%, auto, auto, 36px);
|
||||
@include absolute(6px, auto, auto, 36px);
|
||||
@include center-text(auto, 11px, 21px);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
.tree__item & {
|
||||
padding-left: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +39,7 @@
|
|||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
&:not(.tree__action--disabled):hover {
|
||||
background-color: $color-neutral-100;
|
||||
}
|
||||
|
||||
|
@ -67,6 +68,10 @@
|
|||
&.tree__link--group {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tree__action--disabled &:hover {
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__icon {
|
||||
|
|
|
@ -67,6 +67,42 @@
|
|||
</nuxt-link>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="isStaff" class="tree__item">
|
||||
<div
|
||||
class="tree__action sidebar__action"
|
||||
:class="{ 'tree__action--disabled': isAdminPage }"
|
||||
>
|
||||
<a class="tree__link" @click.prevent="admin()">
|
||||
<i class="tree__icon fas fa-users-cog"></i>
|
||||
<span class="sidebar__item-name">Admin</span>
|
||||
</a>
|
||||
</div>
|
||||
<ul v-show="isAdminPage" class="tree sidebar__tree">
|
||||
<li
|
||||
v-for="adminType in adminTypes"
|
||||
:key="adminType.type"
|
||||
class="tree__item"
|
||||
:class="{
|
||||
active: $route.matched.some(
|
||||
({ name }) => name === adminType.routeName
|
||||
),
|
||||
}"
|
||||
>
|
||||
<div class="tree__action sidebar__action">
|
||||
<nuxt-link
|
||||
:to="{ name: adminType.routeName }"
|
||||
class="tree__link"
|
||||
>
|
||||
<i
|
||||
class="tree__icon fas"
|
||||
:class="'fa-' + adminType.iconClass"
|
||||
></i>
|
||||
<span class="sidebar__item-name">{{ adminType.name }}</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<template v-if="hasSelectedGroup && !isCollapsed">
|
||||
<li class="tree__item margin-top-2">
|
||||
<div class="tree__action">
|
||||
|
@ -208,12 +244,26 @@ export default {
|
|||
this.selectedGroup
|
||||
)
|
||||
},
|
||||
adminTypes() {
|
||||
return this.$registry.getAll('admin')
|
||||
},
|
||||
/**
|
||||
* Indicates whether the current user is visiting an admin page.
|
||||
*/
|
||||
isAdminPage() {
|
||||
return Object.values(this.adminTypes).some((adminType) => {
|
||||
return this.$route.matched.some(
|
||||
({ name }) => name === adminType.routeName
|
||||
)
|
||||
})
|
||||
},
|
||||
...mapState({
|
||||
allApplications: (state) => state.application.items,
|
||||
groups: (state) => state.group.items,
|
||||
selectedGroup: (state) => state.group.selected,
|
||||
}),
|
||||
...mapGetters({
|
||||
isStaff: 'auth/isStaff',
|
||||
name: 'auth/getName',
|
||||
email: 'auth/getUsername',
|
||||
hasSelectedGroup: 'group/hasSelected',
|
||||
|
@ -225,6 +275,24 @@ export default {
|
|||
this.$store.dispatch('auth/logoff')
|
||||
this.$nuxt.$router.push({ name: 'login' })
|
||||
},
|
||||
/**
|
||||
* Called when the user clicks on the admin menu. Because there isn't an
|
||||
* admin page it will navigate to the route of the first registered admin
|
||||
* type.
|
||||
*/
|
||||
admin() {
|
||||
// If the user is already on an admin page we don't have to do anything because
|
||||
// the link is disabled.
|
||||
if (this.isAdminPage) {
|
||||
return
|
||||
}
|
||||
|
||||
const types = Object.values(this.adminTypes)
|
||||
|
||||
if (types.length > 0) {
|
||||
this.$nuxt.$router.push({ name: types[0].routeName })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
Notifications,
|
||||
Sidebar,
|
||||
},
|
||||
middleware: ['authenticated', 'groupsAndApplications'],
|
||||
middleware: ['settings', 'authenticated', 'groupsAndApplications'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isCollapsed: 'sidebar/isCollapsed',
|
||||
|
|
|
@ -15,5 +15,6 @@ import Notifications from '@baserow/modules/core/components/notifications/Notifi
|
|||
|
||||
export default {
|
||||
components: { Notifications },
|
||||
middleware: ['settings'],
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import settings from '@baserow/modules/core/middleware/settings'
|
||||
import authentication from '@baserow/modules/core/middleware/authentication'
|
||||
import authenticated from '@baserow/modules/core/middleware/authenticated'
|
||||
import staff from '@baserow/modules/core/middleware/staff'
|
||||
import groupsAndApplications from '@baserow/modules/core/middleware/groupsAndApplications'
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
import Middleware from './middleware'
|
||||
|
||||
Middleware.settings = settings
|
||||
Middleware.authentication = authentication
|
||||
Middleware.authenticated = authenticated
|
||||
Middleware.staff = staff
|
||||
Middleware.groupsAndApplications = groupsAndApplications
|
||||
|
|
11
web-frontend/modules/core/middleware/settings.js
Normal file
11
web-frontend/modules/core/middleware/settings.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* This middleware makes sure the settings are fetched and available in the store.
|
||||
*/
|
||||
export default async function ({ store, req }) {
|
||||
// If nuxt generate, pass this middleware
|
||||
if (process.server && !req) return
|
||||
|
||||
if (!store.getters['settings/isLoaded']) {
|
||||
await store.dispatch('settings/load')
|
||||
}
|
||||
}
|
13
web-frontend/modules/core/middleware/staff.js
Normal file
13
web-frontend/modules/core/middleware/staff.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* This middleware makes sure that the current user is admin else a 403 error
|
||||
* will be shown to the user.
|
||||
*/
|
||||
export default function ({ store, req, error }) {
|
||||
// If nuxt generate, pass this middleware
|
||||
if (process.server && !req) return
|
||||
|
||||
// If the user is not staff we want to show a forbidden error.
|
||||
if (!store.getters['auth/isStaff']) {
|
||||
return error({ statusCode: 403, message: 'Forbidden.' })
|
||||
}
|
||||
}
|
52
web-frontend/modules/core/pages/admin/settings.vue
Normal file
52
web-frontend/modules/core/pages/admin/settings.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="layout__col-2-scroll">
|
||||
<div class="admin-settings">
|
||||
<h1>Admin settings</h1>
|
||||
<div class="admin-settings__group">
|
||||
<h2 class="admin-settings__group-title">Signup restrictions</h2>
|
||||
<div class="admin-settings__item">
|
||||
<div class="admin-settings__label">
|
||||
<div class="admin-settings__name">Allow creating new accounts</div>
|
||||
<div class="admin-settings__description">
|
||||
By default, any user visiting your Baserow domain can sign up for
|
||||
a new account.
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-settings__control">
|
||||
<SwitchInput
|
||||
:value="settings.allow_new_signups"
|
||||
:large="true"
|
||||
@input="updateSettings({ allow_new_signups: $event })"
|
||||
>enabled</SwitchInput
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
layout: 'app',
|
||||
middleware: 'staff',
|
||||
computed: {
|
||||
...mapGetters({
|
||||
settings: 'settings/get',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async updateSettings(values) {
|
||||
try {
|
||||
await this.$store.dispatch('settings/update', values)
|
||||
} catch (error) {
|
||||
notifyIf(error, 'settings')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
<div class="actions">
|
||||
<ul class="action__links">
|
||||
<li>
|
||||
<li v-if="settings.allow_new_signups">
|
||||
<nuxt-link :to="{ name: 'signup' }"> Sign up </nuxt-link>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -87,6 +87,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import { required, email } from 'vuelidate/lib/validators'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import groupInvitationToken from '@baserow/modules/core/mixins/groupInvitationToken'
|
||||
|
@ -115,6 +117,11 @@ export default {
|
|||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
settings: 'settings/get',
|
||||
}),
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.invitation !== null) {
|
||||
this.credentials.email = this.invitation.email
|
||||
|
|
|
@ -1,122 +1,144 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="box__title">Sign up</h1>
|
||||
<div
|
||||
v-if="invitation !== null"
|
||||
class="alert alert--simple alert-primary alert--has-icon"
|
||||
>
|
||||
<div class="alert__icon">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
<template v-if="!settings.allow_new_signups">
|
||||
<div class="alert alert--simple alert--error alert--has-icon">
|
||||
<div class="alert__icon">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
</div>
|
||||
<div class="alert__title">Sign up is disabled</div>
|
||||
<p class="alert__content">
|
||||
It's not possible to create an account because it has been disabled.
|
||||
</p>
|
||||
</div>
|
||||
<div class="alert__title">Invitation</div>
|
||||
<p class="alert__content">
|
||||
<strong>{{ invitation.invited_by }}</strong> has invited you to join
|
||||
<strong>{{ invitation.group }}</strong
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<Error :error="error"></Error>
|
||||
<form @submit.prevent="register">
|
||||
<div class="control">
|
||||
<label class="control__label">E-mail address</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-if="invitation !== null"
|
||||
ref="email"
|
||||
type="email"
|
||||
class="input input--large"
|
||||
disabled
|
||||
:value="account.email"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
ref="email"
|
||||
v-model="account.email"
|
||||
:class="{ 'input--error': $v.account.email.$error }"
|
||||
type="text"
|
||||
class="input input--large"
|
||||
@blur="$v.account.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.email.$error" class="error">
|
||||
Please enter a valid e-mail address.
|
||||
<nuxt-link
|
||||
:to="{ name: 'login' }"
|
||||
class="button button--large button--primary"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to login
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="invitation !== null"
|
||||
class="alert alert--simple alert--primary alert--has-icon"
|
||||
>
|
||||
<div class="alert__icon">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
</div>
|
||||
<div class="alert__title">Invitation</div>
|
||||
<p class="alert__content">
|
||||
<strong>{{ invitation.invited_by }}</strong> has invited you to join
|
||||
<strong>{{ invitation.group }}</strong
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<Error :error="error"></Error>
|
||||
<form @submit.prevent="register">
|
||||
<div class="control">
|
||||
<label class="control__label">E-mail address</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-if="invitation !== null"
|
||||
ref="email"
|
||||
type="email"
|
||||
class="input input--large"
|
||||
disabled
|
||||
:value="account.email"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
ref="email"
|
||||
v-model="account.email"
|
||||
:class="{ 'input--error': $v.account.email.$error }"
|
||||
type="text"
|
||||
class="input input--large"
|
||||
@blur="$v.account.email.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.email.$error" class="error">
|
||||
Please enter a valid e-mail address.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Your name</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.name"
|
||||
:class="{ 'input--error': $v.account.name.$error }"
|
||||
type="text"
|
||||
class="input input--large"
|
||||
@blur="$v.account.name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.name.$error" class="error">
|
||||
A minimum of two characters is required here.
|
||||
<div class="control">
|
||||
<label class="control__label">Your name</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.name"
|
||||
:class="{ 'input--error': $v.account.name.$error }"
|
||||
type="text"
|
||||
class="input input--large"
|
||||
@blur="$v.account.name.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.name.$error" class="error">
|
||||
A minimum of two characters is required here.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Password</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.password"
|
||||
:class="{ 'input--error': $v.account.password.$error }"
|
||||
type="password"
|
||||
class="input input--large"
|
||||
@blur="$v.account.password.$touch()"
|
||||
/>
|
||||
<div
|
||||
v-if="$v.account.password.$error && !$v.account.password.required"
|
||||
class="error"
|
||||
<div class="control">
|
||||
<label class="control__label">Password</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.password"
|
||||
:class="{ 'input--error': $v.account.password.$error }"
|
||||
type="password"
|
||||
class="input input--large"
|
||||
@blur="$v.account.password.$touch()"
|
||||
/>
|
||||
<div
|
||||
v-if="$v.account.password.$error && !$v.account.password.required"
|
||||
class="error"
|
||||
>
|
||||
A password is required.
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
$v.account.password.$error && !$v.account.password.maxLength
|
||||
"
|
||||
class="error"
|
||||
>
|
||||
A maximum of
|
||||
{{ $v.account.password.$params.maxLength.max }} characters is
|
||||
allowed here.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Repeat password</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.passwordConfirm"
|
||||
:class="{ 'input--error': $v.account.passwordConfirm.$error }"
|
||||
type="password"
|
||||
class="input input--large"
|
||||
@blur="$v.account.passwordConfirm.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.passwordConfirm.$error" class="error">
|
||||
This field must match your password field.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ul class="action__links">
|
||||
<li>
|
||||
<nuxt-link :to="{ name: 'login' }">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
:class="{ 'button--loading': loading }"
|
||||
class="button button--large"
|
||||
:disabled="loading"
|
||||
>
|
||||
A password is required.
|
||||
</div>
|
||||
<div
|
||||
v-if="$v.account.password.$error && !$v.account.password.maxLength"
|
||||
class="error"
|
||||
>
|
||||
A maximum of
|
||||
{{ $v.account.password.$params.maxLength.max }} characters is
|
||||
allowed here.
|
||||
</div>
|
||||
Sign up
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="control__label">Repeat password</label>
|
||||
<div class="control__elements">
|
||||
<input
|
||||
v-model="account.passwordConfirm"
|
||||
:class="{ 'input--error': $v.account.passwordConfirm.$error }"
|
||||
type="password"
|
||||
class="input input--large"
|
||||
@blur="$v.account.passwordConfirm.$touch()"
|
||||
/>
|
||||
<div v-if="$v.account.passwordConfirm.$error" class="error">
|
||||
This field must match your password field.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ul class="action__links">
|
||||
<li>
|
||||
<nuxt-link :to="{ name: 'login' }">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
:class="{ 'button--loading': loading }"
|
||||
class="button button--large"
|
||||
:disabled="loading"
|
||||
>
|
||||
Sign up
|
||||
<i class="fas fa-user-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -132,6 +154,7 @@ import {
|
|||
import { ResponseErrorMessage } from '@baserow/modules/core/plugins/clientHandler'
|
||||
import groupInvitationToken from '@baserow/modules/core/mixins/groupInvitationToken'
|
||||
import error from '@baserow/modules/core/mixins/error'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
mixins: [error, groupInvitationToken],
|
||||
|
@ -152,6 +175,11 @@ export default {
|
|||
title: 'Create new account',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
settings: 'settings/get',
|
||||
}),
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.invitation !== null) {
|
||||
this.account.email = this.invitation.email
|
||||
|
|
|
@ -7,7 +7,9 @@ import {
|
|||
UploadFileUserFileUploadType,
|
||||
UploadViaURLUserFileUploadType,
|
||||
} from '@baserow/modules/core/userFileUploadTypes'
|
||||
import { SettingsAdminType } from '@baserow/modules/core/adminTypes'
|
||||
|
||||
import settingsStore from '@baserow/modules/core/store/settings'
|
||||
import applicationStore from '@baserow/modules/core/store/application'
|
||||
import authStore from '@baserow/modules/core/store/auth'
|
||||
import groupStore from '@baserow/modules/core/store/group'
|
||||
|
@ -27,8 +29,10 @@ export default ({ store, app }, inject) => {
|
|||
registry.register('settings', new PasswordSettingsType())
|
||||
registry.register('userFileUpload', new UploadFileUserFileUploadType())
|
||||
registry.register('userFileUpload', new UploadViaURLUserFileUploadType())
|
||||
registry.register('admin', new SettingsAdminType())
|
||||
inject('registry', registry)
|
||||
|
||||
store.registerModule('settings', settingsStore)
|
||||
store.registerModule('application', applicationStore)
|
||||
store.registerModule('auth', authStore)
|
||||
store.registerModule('group', groupStore)
|
||||
|
|
|
@ -36,6 +36,11 @@ export const routes = [
|
|||
path: '/group-invitation/:token',
|
||||
component: path.resolve(__dirname, 'pages/groupInvitation.vue'),
|
||||
},
|
||||
{
|
||||
name: 'admin-settings',
|
||||
path: '/admin/settings',
|
||||
component: path.resolve(__dirname, 'pages/admin/settings.vue'),
|
||||
},
|
||||
{
|
||||
name: 'style-guide',
|
||||
path: '/style-guide',
|
||||
|
|
10
web-frontend/modules/core/services/settings.js
Normal file
10
web-frontend/modules/core/services/settings.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default (client) => {
|
||||
return {
|
||||
get() {
|
||||
return client.get('/settings/')
|
||||
},
|
||||
update(values) {
|
||||
return client.patch('/settings/update/', values)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -145,6 +145,9 @@ export const getters = {
|
|||
getUsername(state) {
|
||||
return state.user ? state.user.username : ''
|
||||
},
|
||||
isStaff(state) {
|
||||
return state.user ? state.user.is_staff : false
|
||||
},
|
||||
/**
|
||||
* Returns the amount of seconds it will take before the tokes expires.
|
||||
* @TODO figure out what happens if the browser and server time are not in
|
||||
|
|
55
web-frontend/modules/core/store/settings.js
Normal file
55
web-frontend/modules/core/store/settings.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import SettingsService from '@baserow/modules/core/services/settings'
|
||||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
|
||||
export const state = () => ({
|
||||
loaded: false,
|
||||
settings: {},
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
SET_SETTINGS(state, values) {
|
||||
state.settings = values
|
||||
},
|
||||
UPDATE_SETTINGS(state, values) {
|
||||
state.settings = Object.assign({}, state.settings, values)
|
||||
},
|
||||
SET_LOADED(state, value) {
|
||||
state.loaded = value
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async load({ commit }) {
|
||||
const { data } = await SettingsService(this.$client).get()
|
||||
commit('SET_SETTINGS', data)
|
||||
commit('SET_LOADED', true)
|
||||
},
|
||||
async update({ commit, getters }, values) {
|
||||
const oldValues = clone(getters.get)
|
||||
commit('UPDATE_SETTINGS', values)
|
||||
|
||||
try {
|
||||
await SettingsService(this.$client).update(values)
|
||||
} catch (e) {
|
||||
commit('SET_SETTINGS', oldValues)
|
||||
throw e
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
isLoaded(state) {
|
||||
return state.loaded
|
||||
},
|
||||
get(state) {
|
||||
return state.settings
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
}
|
Loading…
Add table
Reference in a new issue