mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +00:00
added registration for a new user
This commit is contained in:
parent
64b515d849
commit
3a473f44f5
22 changed files with 261 additions and 49 deletions
Makefile
backend
src/baserow
api/v0
config
user
tests/baserow
web-frontend
config
pages/login
services
store
test/pages
2
Makefile
2
Makefile
|
@ -31,3 +31,5 @@ lint-backend:
|
||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
(cd backend && pytest tests) || exit;
|
(cd backend && pytest tests) || exit;
|
||||||
|
|
||||||
|
make lint-and-test: lint-backend lint-web-frontend test-backend test-web-frontend
|
||||||
|
|
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
|
|
@ -1,10 +0,0 @@
|
||||||
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 = ('id', 'first_name', 'username')
|
|
|
@ -1,18 +1,10 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from rest_framework import routers
|
from .user import urls as user_urls
|
||||||
from rest_framework_jwt.views import (
|
|
||||||
obtain_jwt_token, refresh_jwt_token, verify_jwt_token
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app_name = 'baserow.api.v0'
|
app_name = 'baserow.api.v0'
|
||||||
router = routers.DefaultRouter()
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^token-auth/', obtain_jwt_token),
|
path('user/', include(user_urls, namespace='user'))
|
||||||
url(r'^token-refresh/', refresh_jwt_token),
|
|
||||||
url(r'^token-verify/', verify_jwt_token),
|
|
||||||
path('', include(router.urls)),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .serializers.user import UserSerializer
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
|
|
||||||
def jwt_response_payload_handler(token, user=None, request=None):
|
def jwt_response_payload_handler(token, user=None, request=None):
|
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='user')
|
||||||
|
]
|
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)
|
|
@ -132,5 +132,6 @@ JWT_AUTH = {
|
||||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
|
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
|
||||||
'JWT_ALLOW_REFRESH': True,
|
'JWT_ALLOW_REFRESH': True,
|
||||||
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||||
'JWT_RESPONSE_PAYLOAD_HANDLER': 'baserow.api.v0.jwt.jwt_response_payload_handler'
|
'JWT_RESPONSE_PAYLOAD_HANDLER': 'baserow.api.v0.user.jwt.'
|
||||||
|
'jwt_response_payload_handler'
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@ from django.conf.urls import url
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^api/', include('baserow.api.v0.urls', namespace='api')),
|
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
|
|
@ -1,4 +1,2 @@
|
||||||
def test_homepage(client):
|
def test_homepage():
|
||||||
response = client.get('/api/')
|
pass
|
||||||
assert response.json()['detail'] == 'Authentication credentials were not provided.'
|
|
||||||
assert response.status_code == 401
|
|
||||||
|
|
|
@ -38,6 +38,6 @@ export default {
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
// The API base url, this will be prepended to the urls of the remote calls.
|
// The API base url, this will be prepended to the urls of the remote calls.
|
||||||
baseUrl: 'http://localhost:8000'
|
baseUrl: 'http://localhost:8000/api/v0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,11 +52,6 @@
|
||||||
Sign up
|
Sign up
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<nuxt-link :to="{ name: 'index' }">
|
|
||||||
Index
|
|
||||||
</nuxt-link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<button
|
<button
|
||||||
:class="{ 'button-loading': loading }"
|
:class="{ 'button-loading': loading }"
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="box-title">Sign up</h1>
|
<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">
|
<form @submit.prevent="register">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="control-label">E-mail address</label>
|
<label class="control-label">E-mail address</label>
|
||||||
|
@ -108,12 +120,13 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
error: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
account: {
|
account: {
|
||||||
email: '',
|
email: 'lol@lol.nl',
|
||||||
name: '',
|
name: 'lol',
|
||||||
password: '',
|
password: 'lol',
|
||||||
passwordConfirm: ''
|
passwordConfirm: 'lol'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -122,6 +135,23 @@ export default {
|
||||||
this.$v.$touch()
|
this.$v.$touch()
|
||||||
if (!this.$v.$invalid) {
|
if (!this.$v.$invalid) {
|
||||||
this.loading = true
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,22 @@ import { client } from './client'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login(username, password) {
|
login(username, password) {
|
||||||
return client.post('/api/token-auth/', {
|
return client.post('/user/token-auth/', {
|
||||||
username: username,
|
username,
|
||||||
password: password
|
password
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refresh(token) {
|
refresh(token) {
|
||||||
return client.post('/api/token-refresh/', {
|
return client.post('/user/token-refresh/', {
|
||||||
token: token
|
token
|
||||||
|
})
|
||||||
|
},
|
||||||
|
register(email, name, password, authenticate = true) {
|
||||||
|
return client.post('/user/', {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
authenticate
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,24 @@ export const client = axios.create({
|
||||||
'Content-Type': '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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -36,6 +36,19 @@ export const actions = {
|
||||||
dispatch('startRefreshTimeout')
|
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
|
* 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
|
* new refresh timeout. If unsuccessful the existing cookie and user data is
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import Index from '@/pages/index.vue'
|
|
||||||
|
|
||||||
describe('Home', () => {
|
describe('Home', () => {
|
||||||
test('is a Vue instance', () => {
|
test('is a Vue instance', () => {})
|
||||||
const wrapper = mount(Index)
|
|
||||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue