diff --git a/etebase_fastapi/authentication.py b/etebase_fastapi/authentication.py index 742f101..5650652 100644 --- a/etebase_fastapi/authentication.py +++ b/etebase_fastapi/authentication.py @@ -25,7 +25,7 @@ from django_etebase.token_auth.models import AuthToken from django_etebase.token_auth.models import get_default_expiry from django_etebase.utils import create_user, get_user_queryset, CallbackContext from django_etebase.views import msgpack_encode, msgpack_decode -from .exceptions import AuthenticationFailed, transform_validation_error, ValidationError +from .exceptions import AuthenticationFailed, transform_validation_error, HttpError from .msgpack import MsgpackRoute from .utils import BaseModel @@ -207,20 +207,20 @@ def validate_login_request( challenge_data = msgpack_decode(box.decrypt(validated_data.challenge)) now = int(datetime.now().timestamp()) if validated_data.action != expected_action: - raise ValidationError("wrong_action", f'Expected "{challenge_sent_to_user.response}" but got something else') + raise HttpError("wrong_action", f'Expected "{challenge_sent_to_user.response}" but got something else') elif now - challenge_data["timestamp"] > app_settings.CHALLENGE_VALID_SECONDS: - raise ValidationError("challenge_expired", "Login challenge has expired") + raise HttpError("challenge_expired", "Login challenge has expired") elif challenge_data["userId"] != user.id: - raise ValidationError("wrong_user", "This challenge is for the wrong user") + raise HttpError("wrong_user", "This challenge is for the wrong user") elif not settings.DEBUG and validated_data.host.split(":", 1)[0] != host_from_request: - raise ValidationError( + raise HttpError( "wrong_host", f'Found wrong host name. Got: "{validated_data.host}" expected: "{host_from_request}"' ) verify_key = nacl.signing.VerifyKey(bytes(user.userinfo.loginPubkey), encoder=nacl.encoding.RawEncoder) try: verify_key.verify(challenge_sent_to_user.response, challenge_sent_to_user.signature) except nacl.exceptions.BadSignatureError: - raise ValidationError("login_bad_signature", "Wrong password for user.", status.HTTP_401_UNAUTHORIZED) + raise HttpError("login_bad_signature", "Wrong password for user.", status.HTTP_401_UNAUTHORIZED) @authentication_router.get("/is_etebase/") @@ -269,7 +269,7 @@ def dashboard_url(user: User = Depends(get_authenticated_user)): # XXX-TOM get_dashboard_url = app_settings.DASHBOARD_URL_FUNC if get_dashboard_url is None: - raise ValidationError("not_supported", "This server doesn't have a user dashboard.") + raise HttpError("not_supported", "This server doesn't have a user dashboard.") ret = { "url": get_dashboard_url(request, *args, **kwargs), @@ -301,7 +301,7 @@ def signup_save(data: SignupIn, request: Request) -> User: raise EtebaseValidationError("generic", str(e)) if hasattr(instance, "userinfo"): - raise ValidationError("user_exists", "User already exists", status_code=status.HTTP_409_CONFLICT) + raise HttpError("user_exists", "User already exists", status_code=status.HTTP_409_CONFLICT) models.UserInfo.objects.create(**data.dict(exclude={"user"}), owner=instance) return instance diff --git a/etebase_fastapi/collection.py b/etebase_fastapi/collection.py index 8b69708..7f01682 100644 --- a/etebase_fastapi/collection.py +++ b/etebase_fastapi/collection.py @@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, status from django_etebase import models from .authentication import get_authenticated_user -from .exceptions import ValidationError, transform_validation_error, PermissionDenied +from .exceptions import HttpError, transform_validation_error, PermissionDenied from .msgpack import MsgpackRoute from .stoken_handler import filter_by_stoken_and_limit, filter_by_stoken, get_stoken_obj, get_queryset_stoken from .utils import get_object_or_404, Context, Prefetch, PrefetchQuery, is_collection_admin, BaseModel @@ -144,7 +144,7 @@ class ItemDepIn(BaseModel): item = models.CollectionItem.objects.get(uid=self.uid) etag = self.etag if item.etag != etag: - raise ValidationError( + raise HttpError( "wrong_etag", "Wrong etag. Expected {} got {}".format(item.etag, etag), status_code=status.HTTP_409_CONFLICT, @@ -274,7 +274,7 @@ def process_revisions_for_item(item: models.CollectionItem, revision_data: Colle chunk_obj.chunkFile.save("IGNORED", ContentFile(content)) chunk_obj.save() else: - raise ValidationError("chunk_no_content", "Tried to create a new chunk without content") + raise HttpError("chunk_no_content", "Tried to create a new chunk without content") chunks_objs.append(chunk_obj) @@ -290,12 +290,12 @@ def process_revisions_for_item(item: models.CollectionItem, revision_data: Colle def _create(data: CollectionIn, user: User): with transaction.atomic(): if data.item.etag is not None: - raise ValidationError("bad_etag", "etag is not null") + raise HttpError("bad_etag", "etag is not null") instance = models.Collection(uid=data.item.uid, owner=user) try: instance.validate_unique() except django_exceptions.ValidationError: - raise ValidationError( + raise HttpError( "unique_uid", "Collection with this uid already exists", status_code=status.HTTP_409_CONFLICT ) instance.save() @@ -355,7 +355,7 @@ def item_create(item_model: CollectionItemIn, collection: models.Collection, val return instance if validate_etag and cur_etag != etag: - raise ValidationError( + raise HttpError( "wrong_etag", "Wrong etag. Expected {} got {}".format(cur_etag, etag), status_code=status.HTTP_409_CONFLICT, @@ -425,7 +425,7 @@ def item_bulk_common(data: ItemBatchIn, user: User, stoken: t.Optional[str], uid collection_object = queryset.select_for_update().get(uid=uid) if stoken is not None and stoken != collection_object.stoken: - raise ValidationError("stale_stoken", "Stoken is too old", status_code=status.HTTP_409_CONFLICT) + raise HttpError("stale_stoken", "Stoken is too old", status_code=status.HTTP_409_CONFLICT) # XXX-TOM: make sure we return compatible errors data.validate_db() @@ -482,7 +482,7 @@ def fetch_updates( item_limit = 200 if len(data) > item_limit: - raise ValidationError("too_many_items", "Request has too many items.", status_code=status.HTTP_400_BAD_REQUEST) + raise HttpError("too_many_items", "Request has too many items.", status_code=status.HTTP_400_BAD_REQUEST) queryset, stoken_rev = filter_by_stoken(stoken, queryset, models.CollectionItem.stoken_annotation) diff --git a/etebase_fastapi/exceptions.py b/etebase_fastapi/exceptions.py index b7bb0e9..2c1757c 100644 --- a/etebase_fastapi/exceptions.py +++ b/etebase_fastapi/exceptions.py @@ -3,19 +3,17 @@ import typing as t from pydantic import BaseModel -from django_etebase.exceptions import EtebaseValidationError - -class ValidationErrorField(BaseModel): +class HttpErrorField(BaseModel): field: str code: str detail: str -class ValidationErrorOut(BaseModel): +class HttpErrorOut(BaseModel): code: str detail: str - errors: t.Optional[t.List[ValidationErrorField]] + errors: t.Optional[t.List[HttpErrorField]] class CustomHttpException(Exception): @@ -59,24 +57,23 @@ class PermissionDenied(CustomHttpException): super().__init__(code=code, detail=detail, status_code=status_code) -class ValidationError(CustomHttpException): +class HttpError(CustomHttpException): def __init__( self, code: str, detail: str, status_code: int = status.HTTP_400_BAD_REQUEST, - field: t.Optional[str] = None, - errors: t.Optional[t.List["ValidationError"]] = None, + errors: t.Optional[t.List["HttpError"]] = None, ): self.errors = errors super().__init__(code=code, detail=detail, status_code=status_code) @property def as_dict(self) -> dict: - return ValidationErrorOut(code=self.code, errors=self.errors, detail=self.detail).dict() + return HttpErrorOut(code=self.code, errors=self.errors, detail=self.detail).dict() -def flatten_errors(field_name, errors) -> t.List[ValidationError]: +def flatten_errors(field_name, errors) -> t.List[HttpError]: ret = [] if isinstance(errors, dict): for error_key in errors: @@ -98,5 +95,5 @@ def transform_validation_error(prefix, err): elif not hasattr(err, "message"): errors = flatten_errors(prefix, err.error_list) else: - raise EtebaseValidationError(err.code, err.message) - raise ValidationError(code="field_errors", detail="Field validations failed.", errors=errors) + raise HttpError(err.code, err.message) + raise HttpError(code="field_errors", detail="Field validations failed.", errors=errors) diff --git a/etebase_fastapi/invitation.py b/etebase_fastapi/invitation.py index ab0bf01..38b74d8 100644 --- a/etebase_fastapi/invitation.py +++ b/etebase_fastapi/invitation.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, status, Request from django_etebase import models from django_etebase.utils import get_user_queryset, CallbackContext from .authentication import get_authenticated_user -from .exceptions import ValidationError, PermissionDenied +from .exceptions import HttpError, PermissionDenied from .msgpack import MsgpackRoute from .utils import get_object_or_404, Context, is_collection_admin, BaseModel @@ -42,7 +42,7 @@ class CollectionInvitationCommon(BaseModel): class CollectionInvitationIn(CollectionInvitationCommon): def validate_db(self, context: Context): if context.user.username == self.username.lower(): - raise ValidationError("no_self_invite", "Inviting yourself is not allowed") + raise HttpError("no_self_invite", "Inviting yourself is not allowed") class CollectionInvitationOut(CollectionInvitationCommon): @@ -186,7 +186,7 @@ def outgoing_create( **data.dict(exclude={"collection", "username"}), user=to_user, fromMember=member ) except IntegrityError: - raise ValidationError("invitation_exists", "Invitation already exists") + raise HttpError("invitation_exists", "Invitation already exists") @invitation_outgoing_router.get("/", response_model=InvitationListResponse) diff --git a/etebase_fastapi/utils.py b/etebase_fastapi/utils.py index 7168f87..6ea9513 100644 --- a/etebase_fastapi/utils.py +++ b/etebase_fastapi/utils.py @@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model from django_etebase.models import AccessLevels -from .exceptions import ValidationError +from .exceptions import HttpError User = get_user_model() @@ -35,7 +35,7 @@ def get_object_or_404(queryset: QuerySet, **kwargs): try: return queryset.get(**kwargs) except ObjectDoesNotExist as e: - raise ValidationError("does_not_exist", str(e), status_code=status.HTTP_404_NOT_FOUND) + raise HttpError("does_not_exist", str(e), status_code=status.HTTP_404_NOT_FOUND) def is_collection_admin(collection, user):