220 lines
8.3 KiB
Python
220 lines
8.3 KiB
Python
from django.contrib import auth
|
|
from django.db import IntegrityError
|
|
from django.urls import path, include
|
|
from rest_framework import routers, serializers, viewsets
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
|
from rest_framework.authtoken.models import Token
|
|
from rest_framework.authtoken.views import ObtainAuthToken
|
|
from rest_framework.response import Response
|
|
|
|
from authentication.models import ToolshedUser, AccountPreference
|
|
from authentication.signature_auth import SignatureAuthenticationLocal
|
|
from files.models import File
|
|
from files.serializers import FileSerializer
|
|
from hostadmin.models import Domain
|
|
|
|
router = routers.SimpleRouter()
|
|
|
|
# Schema for the account-level preferences a client may store on the server (see
|
|
# AccountPreference). Device-level preferences are never sent here - they stay in the
|
|
# browser's local storage since they describe the device, not the account.
|
|
PREFERENCE_DEFINITIONS = [
|
|
{
|
|
'key': 'ui.compact_mode',
|
|
'type': 'boolean',
|
|
'default': False,
|
|
'label': 'Compact mode',
|
|
'description': 'Show denser item rows and reduce spacing in lists.',
|
|
},
|
|
{
|
|
'key': 'ui.default_search_scope',
|
|
'type': 'enum',
|
|
'options': ['inventory', 'friends', 'all'],
|
|
'default': 'inventory',
|
|
'label': 'Default search scope',
|
|
'description': 'Choose where the global search starts.',
|
|
},
|
|
{
|
|
'key': 'notifications.desktop_enabled',
|
|
'type': 'boolean',
|
|
'default': True,
|
|
'label': 'Desktop notifications',
|
|
'description': 'Enable in-browser notifications for important updates.',
|
|
},
|
|
{
|
|
'key': 'files.max_upload_mb',
|
|
'type': 'integer',
|
|
'default': 25,
|
|
'label': 'Default upload size limit (MB)',
|
|
'description': 'Used as a prefill hint in upload dialogs.',
|
|
},
|
|
{
|
|
'key': 'ui.experimental_flags',
|
|
'type': 'json',
|
|
'default': {},
|
|
'label': 'Experimental flags',
|
|
'description': 'Optional JSON toggles for feature previews.',
|
|
},
|
|
]
|
|
|
|
|
|
class UserAuthToken(ObtainAuthToken):
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
try:
|
|
fullname = request.data.get('username')
|
|
username = fullname.split('@')[0]
|
|
domain = fullname.split('@')[1]
|
|
password = request.data.get('password')
|
|
user = auth.authenticate(username=username, password=password, domain=domain)
|
|
token, created = Token.objects.get_or_create(user=user)
|
|
return Response({
|
|
'token': token.key,
|
|
'key': user.private_key
|
|
})
|
|
except IndexError:
|
|
return Response({
|
|
'error': 'Invalid Credentials'
|
|
}, status=400)
|
|
except IntegrityError:
|
|
return Response({
|
|
'error': 'Invalid Credentials'
|
|
}, status=400)
|
|
|
|
|
|
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
|
class Meta:
|
|
model = ToolshedUser
|
|
fields = ['username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'date_joined']
|
|
|
|
|
|
class UserViewSet(viewsets.ModelViewSet):
|
|
queryset = ToolshedUser.objects.all()
|
|
serializer_class = UserSerializer
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
|
|
|
|
@api_view(['GET', 'PATCH'])
|
|
@permission_classes([IsAuthenticated])
|
|
@authentication_classes([SignatureAuthenticationLocal])
|
|
def getUserInfo(request):
|
|
user = request.user
|
|
if request.method == 'PATCH':
|
|
old_file = user.profile_picture
|
|
if 'profile_picture' in request.data:
|
|
profile_picture = request.data.get('profile_picture')
|
|
if profile_picture is None:
|
|
user.profile_picture = None
|
|
elif type(profile_picture) == dict:
|
|
serializer = FileSerializer(data=profile_picture)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=400)
|
|
user.profile_picture = serializer.save()
|
|
else:
|
|
return Response({'profile_picture': 'Must be null or an object with data and mime_type.'}, status=400)
|
|
elif 'profile_picture_id' in request.data:
|
|
profile_picture_id = request.data.get('profile_picture_id')
|
|
if profile_picture_id is None:
|
|
user.profile_picture = None
|
|
else:
|
|
try:
|
|
user.profile_picture = File.objects.get(id=profile_picture_id)
|
|
except File.DoesNotExist:
|
|
return Response({'profile_picture_id': 'File does not exist.'}, status=400)
|
|
user.save()
|
|
|
|
if old_file and old_file != user.profile_picture and old_file.connected_items.count() == 0 and old_file.profile_picture_users.count() == 0:
|
|
old_file.delete()
|
|
|
|
return Response({
|
|
'username': user.username,
|
|
'domain': user.domain,
|
|
'email': user.email,
|
|
'profile_picture': FileSerializer(user.profile_picture).data if user.profile_picture else None,
|
|
})
|
|
|
|
|
|
@api_view(['POST'])
|
|
@permission_classes([])
|
|
@authentication_classes([])
|
|
def registerUser(request):
|
|
try:
|
|
username = request.data.get('username')
|
|
domain = request.data.get('domain')
|
|
password = request.data.get('password')
|
|
email = request.data.get('email')
|
|
|
|
errors = {}
|
|
if not username:
|
|
errors['username'] = 'Username is required'
|
|
if not domain:
|
|
errors['domain'] = 'Domain is required'
|
|
if not password:
|
|
errors['password'] = 'Password is required'
|
|
if not email:
|
|
errors['email'] = 'Email is required'
|
|
if ToolshedUser.objects.filter(email=email).exists():
|
|
errors['email'] = 'Email already exists'
|
|
if ToolshedUser.objects.filter(username=username, domain=domain).exists():
|
|
errors['username'] = 'Username already exists'
|
|
if errors:
|
|
return Response({'errors': errors}, status=400)
|
|
|
|
Domain.objects.get(name=domain, open_registration=True)
|
|
|
|
user = ToolshedUser.objects.create_user(username, email, password, domain=domain)
|
|
return Response({'username': user.username, 'domain': user.domain})
|
|
except Domain.DoesNotExist:
|
|
return Response({'errors': {'domain': 'Domain does not exist or is not open for registration'}}, status=400)
|
|
|
|
|
|
@api_view(['GET'])
|
|
@permission_classes([])
|
|
@authentication_classes([])
|
|
def preference_definitions(request):
|
|
"""Return the schema (types, defaults, labels) for the account preferences clients may set."""
|
|
return Response(PREFERENCE_DEFINITIONS)
|
|
|
|
|
|
@api_view(['GET', 'PUT'])
|
|
@permission_classes([IsAuthenticated])
|
|
@authentication_classes([SignatureAuthenticationLocal])
|
|
def account_preferences(request):
|
|
"""Get or bulk-upsert the authenticated user's account-level preferences.
|
|
|
|
GET returns the current preferences as a {key: value} dict. PUT accepts a {key: value}
|
|
dict of one or more preferences to set/overwrite; unspecified keys are left untouched.
|
|
"""
|
|
if request.method == 'PUT':
|
|
if not isinstance(request.data, dict):
|
|
return Response({'detail': 'Expected an object of key/value pairs.'}, status=400)
|
|
for key, value in request.data.items():
|
|
AccountPreference.objects.update_or_create(user=request.user, key=key, defaults={'value': value})
|
|
|
|
preferences = {pref.key: pref.value for pref in request.user.preferences.all()}
|
|
return Response(preferences)
|
|
|
|
|
|
@api_view(['DELETE'])
|
|
@permission_classes([IsAuthenticated])
|
|
@authentication_classes([SignatureAuthenticationLocal])
|
|
def account_preference_detail(request, key):
|
|
"""Reset a single account-level preference back to its default by deleting it."""
|
|
AccountPreference.objects.filter(user=request.user, key=key).delete()
|
|
return Response(status=204)
|
|
|
|
|
|
router.register(r'users', UserViewSet)
|
|
|
|
urlpatterns = [
|
|
path('', include(router.urls)),
|
|
path('user/', getUserInfo),
|
|
path('register/', registerUser),
|
|
path('token/', UserAuthToken.as_view()),
|
|
path('preferences/', preference_definitions),
|
|
path('self/preferences/', account_preferences),
|
|
path('self/preferences/<str:key>/', account_preference_detail),
|
|
]
|