Invitations: implement invitations endpoints.
This commit is contained in:
parent
7f90edc511
commit
b70f2b7470
@ -14,6 +14,7 @@ from fastapi import FastAPI, Request
|
|||||||
from .exceptions import CustomHttpException
|
from .exceptions import CustomHttpException
|
||||||
from .authentication import authentication_router
|
from .authentication import authentication_router
|
||||||
from .collection import collection_router
|
from .collection import collection_router
|
||||||
|
from .invitation import invitation_incoming_router, invitation_outgoing_router
|
||||||
from .msgpack import MsgpackResponse
|
from .msgpack import MsgpackResponse
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -21,6 +22,8 @@ VERSION = "v1"
|
|||||||
BASE_PATH = f"/api/{VERSION}"
|
BASE_PATH = f"/api/{VERSION}"
|
||||||
app.include_router(authentication_router, prefix=f"{BASE_PATH}/authentication")
|
app.include_router(authentication_router, prefix=f"{BASE_PATH}/authentication")
|
||||||
app.include_router(collection_router, prefix=f"{BASE_PATH}/collection")
|
app.include_router(collection_router, prefix=f"{BASE_PATH}/collection")
|
||||||
|
app.include_router(invitation_incoming_router, prefix=f"{BASE_PATH}/invitation/incoming")
|
||||||
|
app.include_router(invitation_outgoing_router, prefix=f"{BASE_PATH}/invitation/outgoing")
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
from .test_reset_view import test_reset_view_router
|
from .test_reset_view import test_reset_view_router
|
||||||
|
|
||||||
|
229
etebase_fastapi/invitation.py
Normal file
229
etebase_fastapi/invitation.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import typing as t
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction, IntegrityError
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from fastapi import APIRouter, Depends, status, Request
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
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 .msgpack import MsgpackRoute, MsgpackResponse
|
||||||
|
from .utils import get_object_or_404, Context, is_collection_admin
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
invitation_incoming_router = APIRouter(route_class=MsgpackRoute)
|
||||||
|
invitation_outgoing_router = APIRouter(route_class=MsgpackRoute)
|
||||||
|
default_queryset: QuerySet = models.CollectionInvitation.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfoOut(BaseModel):
|
||||||
|
pubkey: bytes
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionInvitationAcceptIn(BaseModel):
|
||||||
|
collectionType: bytes
|
||||||
|
encryptionKey: bytes
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionInvitationCommon(BaseModel):
|
||||||
|
uid: str
|
||||||
|
version: int
|
||||||
|
accessLevel: models.AccessLevels
|
||||||
|
username: str
|
||||||
|
collection: str
|
||||||
|
signedEncryptionKey: bytes
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionInvitationOut(CollectionInvitationCommon):
|
||||||
|
fromUsername: str
|
||||||
|
fromPubkey: bytes
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_orm(cls: t.Type["CollectionInvitationOut"], obj: models.CollectionInvitation) -> "CollectionInvitationOut":
|
||||||
|
return cls(
|
||||||
|
uid=obj.uid,
|
||||||
|
version=obj.version,
|
||||||
|
accessLevel=obj.accessLevel,
|
||||||
|
username=obj.user.username,
|
||||||
|
collection=obj.collection.uid,
|
||||||
|
fromUsername=obj.fromMember.user.username,
|
||||||
|
fromPubkey=obj.fromMember.user.userinfo.pubkey,
|
||||||
|
signedEncryptionKey=obj.signedEncryptionKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationListResponse(BaseModel):
|
||||||
|
data: t.List[CollectionInvitationOut]
|
||||||
|
iterator: t.Optional[str]
|
||||||
|
done: bool
|
||||||
|
|
||||||
|
|
||||||
|
def get_incoming_queryset(user: User, queryset=default_queryset):
|
||||||
|
return queryset.filter(user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def get_outgoing_queryset(user: User, queryset=default_queryset):
|
||||||
|
return queryset.filter(fromMember__user=user)
|
||||||
|
|
||||||
|
|
||||||
|
def list_common(
|
||||||
|
queryset: QuerySet,
|
||||||
|
iterator: t.Optional[str],
|
||||||
|
limit: int,
|
||||||
|
) -> MsgpackResponse:
|
||||||
|
queryset = queryset.order_by("id")
|
||||||
|
|
||||||
|
if iterator is not None:
|
||||||
|
iterator_obj = get_object_or_404(queryset, uid=iterator)
|
||||||
|
queryset = queryset.filter(id__gt=iterator_obj.id)
|
||||||
|
|
||||||
|
result = list(queryset[: limit + 1])
|
||||||
|
if len(result) < limit + 1:
|
||||||
|
done = True
|
||||||
|
else:
|
||||||
|
done = False
|
||||||
|
result = result[:-1]
|
||||||
|
|
||||||
|
ret_data = result
|
||||||
|
iterator = ret_data[-1].uid if len(result) > 0 else None
|
||||||
|
|
||||||
|
ret = InvitationListResponse(
|
||||||
|
data=ret_data,
|
||||||
|
iterator=iterator,
|
||||||
|
done=done,
|
||||||
|
)
|
||||||
|
return MsgpackResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_incoming_router.get("/", response_model=InvitationListResponse)
|
||||||
|
def incoming_list(
|
||||||
|
iterator: t.Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
return list_common(get_incoming_queryset(user), iterator, limit)
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_incoming_router.get("/{invitation_uid}/", response_model=CollectionInvitationOut)
|
||||||
|
def incoming_get(
|
||||||
|
invitation_uid: str,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
queryset = get_incoming_queryset(user)
|
||||||
|
obj = get_object_or_404(queryset, uid=invitation_uid)
|
||||||
|
ret = CollectionInvitationOut.from_orm(obj)
|
||||||
|
return MsgpackResponse(ret)
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_incoming_router.delete("/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def incoming_delete(
|
||||||
|
invitation_uid: str,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
queryset = get_incoming_queryset(user)
|
||||||
|
obj = get_object_or_404(queryset, uid=invitation_uid)
|
||||||
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_incoming_router.post("/{invitation_uid}/accept/", status_code=status.HTTP_201_CREATED)
|
||||||
|
def incoming_accept(
|
||||||
|
invitation_uid: str,
|
||||||
|
data: CollectionInvitationAcceptIn,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
queryset = get_incoming_queryset(user)
|
||||||
|
invitation = get_object_or_404(queryset, uid=invitation_uid)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
user = invitation.user
|
||||||
|
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=data.collectionType, owner=user)
|
||||||
|
|
||||||
|
models.CollectionMember.objects.create(
|
||||||
|
collection=invitation.collection,
|
||||||
|
stoken=models.Stoken.objects.create(),
|
||||||
|
user=user,
|
||||||
|
accessLevel=invitation.accessLevel,
|
||||||
|
encryptionKey=data.encryptionKey,
|
||||||
|
collectionType=collection_type_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
models.CollectionMemberRemoved.objects.filter(user=invitation.user, collection=invitation.collection).delete()
|
||||||
|
|
||||||
|
invitation.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_outgoing_router.post("/", status_code=status.HTTP_201_CREATED)
|
||||||
|
def outgoing_create(
|
||||||
|
data: CollectionInvitationIn,
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
collection = get_object_or_404(models.Collection.objects, uid=data.collection)
|
||||||
|
to_user = get_object_or_404(
|
||||||
|
get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), username=data.username
|
||||||
|
)
|
||||||
|
|
||||||
|
context = Context(user, None)
|
||||||
|
data.validate_db(context)
|
||||||
|
|
||||||
|
if not is_collection_admin(collection, user):
|
||||||
|
raise PermissionDenied("admin_access_required", "User is not an admin of this collection")
|
||||||
|
|
||||||
|
member = collection.members.get(user=user)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
ret = models.CollectionInvitation.objects.create(
|
||||||
|
**data.dict(exclude={"collection", "username"}), user=to_user, fromMember=member
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
raise ValidationError("invitation_exists", "Invitation already exists")
|
||||||
|
|
||||||
|
return MsgpackResponse(CollectionInvitationOut.from_orm(ret), status_code=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_outgoing_router.get("/", response_model=InvitationListResponse)
|
||||||
|
def outgoing_list(
|
||||||
|
iterator: t.Optional[str] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
return list_common(get_outgoing_queryset(user), iterator, limit)
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_outgoing_router.delete("/{invitation_uid}/", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
def outgoing_delete(
|
||||||
|
invitation_uid: str,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
queryset = get_outgoing_queryset(user)
|
||||||
|
obj = get_object_or_404(queryset, uid=invitation_uid)
|
||||||
|
obj.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@invitation_outgoing_router.get("/fetch_user_profile/", response_model=UserInfoOut)
|
||||||
|
def outgoing_fetch_user_profile(
|
||||||
|
username: str,
|
||||||
|
request: Request,
|
||||||
|
user: User = Depends(get_authenticated_user),
|
||||||
|
):
|
||||||
|
kwargs = {User.USERNAME_FIELD: username.lower()}
|
||||||
|
user = get_object_or_404(get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), **kwargs)
|
||||||
|
user_info = get_object_or_404(models.UserInfo.objects.all(), owner=user)
|
||||||
|
ret = UserInfoOut.from_orm(user_info)
|
||||||
|
return MsgpackResponse(ret)
|
@ -7,6 +7,8 @@ from django.db.models import QuerySet
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from django_etebase.models import AccessLevels
|
||||||
|
|
||||||
from .exceptions import ValidationError
|
from .exceptions import ValidationError
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -26,3 +28,8 @@ def get_object_or_404(queryset: QuerySet, **kwargs):
|
|||||||
return queryset.get(**kwargs)
|
return queryset.get(**kwargs)
|
||||||
except ObjectDoesNotExist as e:
|
except ObjectDoesNotExist as e:
|
||||||
raise ValidationError("does_not_exist", str(e), status_code=status.HTTP_404_NOT_FOUND)
|
raise ValidationError("does_not_exist", str(e), status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
def is_collection_admin(collection, user):
|
||||||
|
member = collection.members.filter(user=user).first()
|
||||||
|
return (member is not None) and (member.accessLevel == AccessLevels.ADMIN)
|
||||||
|
Loading…
Reference in New Issue
Block a user