Finish api for inviting people to your organization(s)
This commit is contained in:
+5
-4
@@ -55,8 +55,8 @@ CREATE TABLE IF NOT EXISTS "membership" (
|
|||||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
"disabled_at" TIMESTAMP,
|
"disabled_at" TIMESTAMP,
|
||||||
"acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE,
|
"acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE,
|
||||||
"organization_id" CHAR(36) NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE,
|
"organization_id" CHAR(36) REFERENCES "organization" ("id") ON DELETE SET NULL,
|
||||||
"user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
"user_id" CHAR(36) REFERENCES "user" ("id") ON DELETE SET NULL
|
||||||
) /* Membership */;
|
) /* Membership */;
|
||||||
CREATE TABLE IF NOT EXISTS "invite" (
|
CREATE TABLE IF NOT EXISTS "invite" (
|
||||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||||
@@ -64,11 +64,12 @@ CREATE TABLE IF NOT EXISTS "invite" (
|
|||||||
"sender" CHAR(36) NOT NULL,
|
"sender" CHAR(36) NOT NULL,
|
||||||
"org_id" CHAR(36) NOT NULL,
|
"org_id" CHAR(36) NOT NULL,
|
||||||
"message" TEXT,
|
"message" TEXT,
|
||||||
"accepted" INT NOT NULL,
|
"accepted" INT NOT NULL DEFAULT 0,
|
||||||
"disabled" INT NOT NULL DEFAULT 0,
|
"disabled" INT NOT NULL DEFAULT 0,
|
||||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
"disabled_at" TIMESTAMP
|
"disabled_at" TIMESTAMP,
|
||||||
|
"acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "aerich" (
|
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
import uuid
|
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
import pytz
|
import pytz
|
||||||
from modules.users.utils import get_current_active_user
|
from modules.users.utils import get_current_active_user
|
||||||
|
from modules.users.models import User
|
||||||
from modules.auth.utils import create_jwt_tokens, get_tokens_from_logged_in_user
|
from modules.auth.utils import create_jwt_tokens, get_tokens_from_logged_in_user
|
||||||
from modules.auth.models import Token
|
from modules.auth.models import Token
|
||||||
from modules.users.models import User
|
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from tortoise.expressions import Q
|
from tortoise.expressions import Q
|
||||||
from config import settings
|
from config import settings
|
||||||
@@ -31,23 +30,31 @@ async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
|||||||
|
|
||||||
Logs the user into our API, creates tokens and passes them back to User.
|
Logs the user into our API, creates tokens and passes them back to User.
|
||||||
"""
|
"""
|
||||||
user: User | None = await User.filter(Q(email=form.username) | Q(username=form.username)).first()
|
user: User | None = await User.filter(
|
||||||
|
Q(email=form.username) | Q(username=form.username)
|
||||||
|
).first()
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=401, detail=account_error)
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=account_error
|
||||||
|
)
|
||||||
|
|
||||||
if user.check_against_password(form.password) is False:
|
if user.check_against_password(form.password) is False:
|
||||||
raise HTTPException(status_code=401, detail=account_error)
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=account_error
|
||||||
|
)
|
||||||
|
|
||||||
if user.disabled is True:
|
if user.disabled is True:
|
||||||
raise HTTPException(status_code=401, detail=account_error)
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail=account_error
|
||||||
|
)
|
||||||
|
|
||||||
tokens = await create_jwt_tokens(user)
|
tokens = await create_jwt_tokens(user)
|
||||||
|
|
||||||
return {"jwt": tokens}
|
return {"jwt": tokens}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout", status_code=204)
|
@router.get("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def logout(user: Annotated[User, Depends(get_current_active_user)]):
|
async def logout(user: Annotated[User, Depends(get_current_active_user)]):
|
||||||
"""
|
"""
|
||||||
Logout
|
Logout
|
||||||
@@ -110,7 +117,9 @@ async def refresh_login(
|
|||||||
return {"jwt": tokens}
|
return {"jwt": tokens}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", status_code=201, response_model=user_model)
|
@router.post(
|
||||||
|
"/register", status_code=status.HTTP_201_CREATED, response_model=user_model
|
||||||
|
)
|
||||||
async def register(user: register_model):
|
async def register(user: register_model):
|
||||||
# Prevent existing users from reapplying for our system.
|
# Prevent existing users from reapplying for our system.
|
||||||
existing_user: User | None = await User.filter(
|
existing_user: User | None = await User.filter(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
import pytz
|
||||||
from tortoise import Model, fields
|
from tortoise import Model, fields
|
||||||
|
|
||||||
|
from modules.users.models import ACL
|
||||||
|
|
||||||
|
|
||||||
class Invite(Model):
|
class Invite(Model):
|
||||||
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
||||||
@@ -8,8 +12,19 @@ class Invite(Model):
|
|||||||
sender: uuid.UUID = fields.UUIDField()
|
sender: uuid.UUID = fields.UUIDField()
|
||||||
org_id: uuid.UUID = fields.UUIDField()
|
org_id: uuid.UUID = fields.UUIDField()
|
||||||
message: str | None = fields.TextField(null=True)
|
message: str | None = fields.TextField(null=True)
|
||||||
accepted: bool = fields.BooleanField()
|
acl: ACL = fields.ForeignKeyField("models.ACL")
|
||||||
|
accepted: bool = fields.BooleanField(default=False)
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||||
disabled_at = fields.DatetimeField(null=True)
|
disabled_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete(self, force: bool = False) -> None:
|
||||||
|
if force:
|
||||||
|
await Model.delete(self)
|
||||||
|
else:
|
||||||
|
self.disabled = True
|
||||||
|
self.disabled_at = datetime.now(tz=pytz.UTC)
|
||||||
|
await self.save()
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,54 @@
|
|||||||
from typing import Annotated, List
|
from typing import Annotated, List
|
||||||
import uuid
|
import uuid
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
|
from modules.organizations.models import Organization
|
||||||
from modules.invitations.models import Invite
|
from modules.invitations.models import Invite
|
||||||
from modules.invitations.schemas import invitation_model
|
from modules.invitations.schemas import invitation_model, send_invitation_for_org
|
||||||
|
|
||||||
from modules.users.models import User
|
from modules.users.models import ACL, Membership, User
|
||||||
from modules.users.utils import get_current_active_user
|
from modules.users.utils import get_current_active_user
|
||||||
|
|
||||||
from tortoise.expressions import Q
|
from tortoise.expressions import Q
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v1/invitations", tags=["invites"])
|
||||||
router = APIRouter(prefix="/api/v1/Users", tags=["User"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[invitation_model])
|
@router.get("/", response_model=List[invitation_model])
|
||||||
async def get_all_invitations(
|
async def get_all_invitations(
|
||||||
user: Annotated[User, Depends(get_current_active_user)],
|
user: Annotated[User, Depends(get_current_active_user)],
|
||||||
) -> List[Invite]:
|
) -> List[Invite]:
|
||||||
invites: List[Invite] | None = await Invite.filter(
|
"""Returns all invitations for user requesting, except disabled invites.
|
||||||
Q(receiver=user.username) | Q(receiver=user.email)
|
|
||||||
|
Args:
|
||||||
|
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Invite]: A list of invitations.
|
||||||
|
"""
|
||||||
|
return await Invite.filter(
|
||||||
|
Q(receiver=user.username) | Q(receiver=user.email) & Q(disabled=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
return invites
|
|
||||||
|
|
||||||
|
@router.delete("/{invitation_id}", response_model=invitation_model)
|
||||||
@router.get("/accept/{invitation_id}", response_model=invitation_model)
|
async def delete_invitation(
|
||||||
async def accept_invitation(
|
|
||||||
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
||||||
) -> Invite:
|
) -> None:
|
||||||
|
"""Removes an invitation you have sent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||||
|
invitation_id (uuid.UUID): UUID for the invitation to be removed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: When invitation doesn't exist return 403.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Invite: The Invitation model.
|
||||||
|
"""
|
||||||
invite: Invite | None = await Invite.get_or_none(
|
invite: Invite | None = await Invite.get_or_none(
|
||||||
Q(id=invitation_id) & (Q(receiver=user.username) | Q(receiver=user.email))
|
Q(id=invitation_id) & Q(sender=user.id) & Q(disabled=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not invite:
|
if not invite:
|
||||||
@@ -39,44 +57,29 @@ async def accept_invitation(
|
|||||||
detail="The invitation doesn't exist or you don't have access to it.",
|
detail="The invitation doesn't exist or you don't have access to it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
invite.accepted = True
|
await invite.delete()
|
||||||
return await invite.save()
|
return invite
|
||||||
|
|
||||||
|
|
||||||
@router.get("/reject/{invitation_id}", response_model=invitation_model)
|
@router.get("/accept/{invitation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def reject_invitation(
|
|
||||||
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
|
||||||
) -> Invite:
|
|
||||||
invite: Invite | None = await Invite.get_or_none(
|
|
||||||
Q(id=invitation_id) & (Q(receiver=user.username) | Q(receiver=user.email))
|
|
||||||
)
|
|
||||||
|
|
||||||
if not invite:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail="The invitation doesn't exist or you don't have access to it.",
|
|
||||||
)
|
|
||||||
|
|
||||||
invite.accepted = False
|
|
||||||
return await invite.save()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/send")
|
|
||||||
async def send_invitation(
|
|
||||||
user: Annotated[User, Depends(get_current_active_user)],
|
|
||||||
):
|
|
||||||
# Check if user is Manager or Higher to send an invitation.
|
|
||||||
# Should send an E-Mail as notification.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cancel/{invitation_id}", response_model=invitation_model)
|
|
||||||
async def accept_invitation(
|
async def accept_invitation(
|
||||||
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Accepts the invitation sent by a different organization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||||
|
invitation_id (uuid.UUID): UUID for the organization that the users wants to add the person to.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Raises exception when invite is not available or disabled.
|
||||||
|
|
||||||
|
"""
|
||||||
invite: Invite | None = await Invite.get_or_none(
|
invite: Invite | None = await Invite.get_or_none(
|
||||||
Q(id=invitation_id) & Q(sender=user.id)
|
Q(id=invitation_id)
|
||||||
)
|
& (Q(receiver=user.username) | Q(receiver=user.email))
|
||||||
|
& Q(disabled=False)
|
||||||
|
).prefetch_related("acl")
|
||||||
|
|
||||||
if not invite:
|
if not invite:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -84,4 +87,128 @@ async def accept_invitation(
|
|||||||
detail="The invitation doesn't exist or you don't have access to it.",
|
detail="The invitation doesn't exist or you don't have access to it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return await invite.delete()
|
if invite.disabled:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="You have already declined the invitation or the invitation was removed, you can't accept it.",
|
||||||
|
)
|
||||||
|
|
||||||
|
invite.accepted = True
|
||||||
|
await invite.save()
|
||||||
|
# Disable invite after accepting, prevent changing it.
|
||||||
|
await invite.delete()
|
||||||
|
|
||||||
|
await Membership.create(
|
||||||
|
user=user, organization=await Organization.get(id=invite.org_id), acl=invite.acl
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/decline/{invitation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def reject_invitation(
|
||||||
|
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
||||||
|
) -> None:
|
||||||
|
"""Declines an invitation to join an organization
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||||
|
invitation_id (uuid.UUID): The UUID of the invitation
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Checks if the invite exists.
|
||||||
|
HTTPException: Checks if the invite has already accepted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Invite: The Invitation model.
|
||||||
|
"""
|
||||||
|
invite: Invite | None = await Invite.get_or_none(
|
||||||
|
Q(id=invitation_id)
|
||||||
|
& (Q(receiver=user.username) | Q(receiver=user.email))
|
||||||
|
& Q(disabled=False)
|
||||||
|
).prefetch_related("acl")
|
||||||
|
|
||||||
|
if not invite:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="The invitation doesn't exist or you don't have access to it.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if invite.accepted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="The invitation was already accepted, you can't remove it.",
|
||||||
|
)
|
||||||
|
|
||||||
|
await invite.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/send", response_model=invitation_model)
|
||||||
|
async def send_invitation(
|
||||||
|
user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
invite_details: send_invitation_for_org,
|
||||||
|
) -> Invite:
|
||||||
|
"""Sends an invitation to e-mail or username.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||||
|
invite_details (send_invitation_for_org): The details for the invitation.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: Checks access to the organization posted.
|
||||||
|
HTTPException: Checks for Manager or Admin permissions and declines if you are not.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Invite: The Invitation model.
|
||||||
|
"""
|
||||||
|
# Should send an E-Mail as notification.
|
||||||
|
membership = await Membership.get_or_none(
|
||||||
|
Q(user=user.id) & Q(organization=invite_details.org_id)
|
||||||
|
).prefetch_related("acl")
|
||||||
|
|
||||||
|
if not membership:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="You do not have access to this organization.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not membership.acl.MANAGE or not membership.acl.ADMIN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="You are not allowed to send invitations for this organization.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user is already part of organization
|
||||||
|
invited_user: User | None = await User.get_or_none(
|
||||||
|
Q(username=invite_details.receiver) | Q(email=invite_details.receiver)
|
||||||
|
)
|
||||||
|
|
||||||
|
if invited_user:
|
||||||
|
user_is_part_of_org = await Membership.get_or_none(
|
||||||
|
Q(user=invited_user) & Q(organization=invite_details.org_id)
|
||||||
|
)
|
||||||
|
if user_is_part_of_org:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="The person you've invited is already part of the organization.",
|
||||||
|
)
|
||||||
|
|
||||||
|
acl = None
|
||||||
|
if invite_details.acl:
|
||||||
|
acl = await ACL.create(
|
||||||
|
READ=invite_details.acl.READ,
|
||||||
|
WRITE=invite_details.acl.WRITE,
|
||||||
|
REPORT=invite_details.acl.REPORT,
|
||||||
|
MANAGE=invite_details.acl.MANAGE,
|
||||||
|
ADMIN=invite_details.acl.ADMIN,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
acl = await ACL.create(
|
||||||
|
READ=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return await Invite.create(
|
||||||
|
receiver=invite_details.receiver,
|
||||||
|
sender=user.id,
|
||||||
|
org_id=invite_details.org_id,
|
||||||
|
message=invite_details.message,
|
||||||
|
acl=acl,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import uuid
|
||||||
|
from pydantic import BaseModel
|
||||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||||
|
|
||||||
from modules.invitations.models import Invite
|
from modules.invitations.models import Invite
|
||||||
|
|
||||||
invitation_model = pydantic_model_creator(Invite)
|
invitation_model = pydantic_model_creator(Invite)
|
||||||
|
|
||||||
|
class acl_model(BaseModel):
|
||||||
|
READ: bool
|
||||||
|
WRITE: bool
|
||||||
|
REPORT: bool
|
||||||
|
MANAGE: bool
|
||||||
|
ADMIN: bool
|
||||||
|
|
||||||
|
class send_invitation_for_org(BaseModel):
|
||||||
|
org_id: uuid.UUID
|
||||||
|
receiver: str
|
||||||
|
acl: acl_model | None
|
||||||
|
message: str | None
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ class Membership(Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
||||||
organization: Organization = fields.ForeignKeyField("models.Organization")
|
organization: Organization = fields.ForeignKeyField("models.Organization", null=True, on_delete=fields.SET_NULL)
|
||||||
user: User = fields.ForeignKeyField("models.User")
|
user: User = fields.ForeignKeyField("models.User", null=True, on_delete=fields.SET_NULL)
|
||||||
acl: ACL = fields.ForeignKeyField("models.ACL")
|
acl: ACL = fields.ForeignKeyField("models.ACL")
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from tests.base_test import Test
|
from tests.base_test import Test
|
||||||
from modules.users.models import User
|
from modules.users.models import User
|
||||||
import pytest # type: ignore
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from config import settings
|
from config import settings
|
||||||
from unittest.mock import ANY
|
from unittest.mock import ANY
|
||||||
|
|||||||
@@ -0,0 +1,457 @@
|
|||||||
|
from unittest.mock import ANY
|
||||||
|
from httpx import AsyncClient
|
||||||
|
from modules.users.models import ACL, Membership
|
||||||
|
from tests.base_test import Test
|
||||||
|
|
||||||
|
|
||||||
|
class TestInvitationalRoutes(Test):
|
||||||
|
async def test_send_invitations(self, client: AsyncClient, create_user_with_org):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin12@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user1231@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user1231@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user1231@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == [
|
||||||
|
{
|
||||||
|
"id": ANY,
|
||||||
|
"receiver": "user1231@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"accepted": False,
|
||||||
|
"disabled": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"modified_at": ANY,
|
||||||
|
"disabled_at": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async def test_cannot_see_others_invitations(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin99@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user18@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user1231@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user1231@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == []
|
||||||
|
|
||||||
|
async def test_accept_sent_invitations(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin191@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user8@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user8@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user8@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == [
|
||||||
|
{
|
||||||
|
"id": ANY,
|
||||||
|
"receiver": "user8@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"accepted": False,
|
||||||
|
"disabled": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"modified_at": ANY,
|
||||||
|
"disabled_at": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
invite_id = user_invites.json()[0]["id"]
|
||||||
|
|
||||||
|
accept_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/accept/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accept_invite.status_code == 204
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
# When an invite has been accepted, it should be removed.
|
||||||
|
assert user_invites.json() == []
|
||||||
|
|
||||||
|
async def test_decline_sent_invitations(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin11@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user98@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user98@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user98@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == [
|
||||||
|
{
|
||||||
|
"id": ANY,
|
||||||
|
"receiver": "user98@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"accepted": False,
|
||||||
|
"disabled": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"modified_at": ANY,
|
||||||
|
"disabled_at": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
invite_id = user_invites.json()[0]["id"]
|
||||||
|
|
||||||
|
accept_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/decline/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accept_invite.status_code == 204
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == []
|
||||||
|
|
||||||
|
async def test_prevent_accepting_when_declined_sent_invitations(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin9612@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user11918@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user11918@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user11918@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == [
|
||||||
|
{
|
||||||
|
"id": ANY,
|
||||||
|
"receiver": "user11918@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"accepted": False,
|
||||||
|
"disabled": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"modified_at": ANY,
|
||||||
|
"disabled_at": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
invite_id = user_invites.json()[0]["id"]
|
||||||
|
|
||||||
|
accept_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/decline/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accept_invite.status_code == 204
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == []
|
||||||
|
|
||||||
|
accept_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/accept/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accept_invite.status_code == 403
|
||||||
|
assert accept_invite.json() == {
|
||||||
|
"detail": "The invitation doesn't exist or you don't have access to it."
|
||||||
|
}
|
||||||
|
|
||||||
|
async def test_prevent_declining_when_accepted_sent_invitations(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
admin, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="superadmin9712@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
_, _, _, usertokens = await create_user_with_org(email="user14918@localhost.com")
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user14918@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 200
|
||||||
|
assert invite.json() == {
|
||||||
|
"accepted": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_at": None,
|
||||||
|
"id": ANY,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"modified_at": ANY,
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "user14918@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == [
|
||||||
|
{
|
||||||
|
"id": ANY,
|
||||||
|
"receiver": "user14918@localhost.com",
|
||||||
|
"sender": str(admin.id),
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
"accepted": False,
|
||||||
|
"disabled": False,
|
||||||
|
"created_at": ANY,
|
||||||
|
"modified_at": ANY,
|
||||||
|
"disabled_at": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
invite_id = user_invites.json()[0]["id"]
|
||||||
|
|
||||||
|
accept_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/accept/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert accept_invite.status_code == 204
|
||||||
|
|
||||||
|
user_invites = await client.get(
|
||||||
|
"https://localhost/api/v1/invitations/",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user_invites.status_code == 200
|
||||||
|
assert user_invites.json() == []
|
||||||
|
|
||||||
|
decline_invite = await client.get(
|
||||||
|
f"https://localhost/api/v1/invitations/accept/{invite_id}",
|
||||||
|
headers={"Authorization": f"Bearer {usertokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert decline_invite.status_code == 403
|
||||||
|
assert decline_invite.json() == {
|
||||||
|
"detail": "The invitation doesn't exist or you don't have access to it."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_prevent_adding_user_whom_already_belongs_to_your_organization(
|
||||||
|
self, client: AsyncClient, create_user_with_org
|
||||||
|
):
|
||||||
|
_, org, _, admintokens = await create_user_with_org(
|
||||||
|
email="us3r123@localhost.com",
|
||||||
|
username="awesomeadmin",
|
||||||
|
password="awesomeadmin",
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user, _, _, _ = await create_user_with_org(email="us3r1234@localhost.com")
|
||||||
|
|
||||||
|
await Membership.create(
|
||||||
|
user=user,
|
||||||
|
organization=org,
|
||||||
|
acl=await ACL.create(READ=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
invite = await client.post(
|
||||||
|
"https://localhost/api/v1/invitations/send",
|
||||||
|
json={
|
||||||
|
"org_id": str(org.id),
|
||||||
|
"receiver": "us3r1234@localhost.com",
|
||||||
|
"acl": None,
|
||||||
|
"message": "Hi! We would like to invite you to our organization.",
|
||||||
|
},
|
||||||
|
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert invite.status_code == 403
|
||||||
|
assert invite.json() == {
|
||||||
|
"detail": "The person you've invited is already part of the organization."
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from modules.users.models import ACL, Membership
|
from modules.users.models import ACL, Membership
|
||||||
from modules.organizations.models import Organization
|
from modules.organizations.models import Organization
|
||||||
from config import settings
|
|
||||||
from unittest.mock import ANY
|
from unittest.mock import ANY
|
||||||
from tests.base_test import Test
|
from tests.base_test import Test
|
||||||
|
|
||||||
crypt = settings.CRYPT
|
|
||||||
|
|
||||||
|
|
||||||
class TestOrganizationRoute(Test):
|
class TestOrganizationRoute(Test):
|
||||||
async def test_get_organizations_from_api(
|
async def test_get_organizations_from_api(
|
||||||
@@ -153,9 +150,7 @@ class TestOrganizationRoute(Test):
|
|||||||
READ=True, WRITE=True, REPORT=True, MANAGE=False, ADMIN=False
|
READ=True, WRITE=True, REPORT=True, MANAGE=False, ADMIN=False
|
||||||
)
|
)
|
||||||
|
|
||||||
await Membership.create(
|
await Membership.create(user=user, organization=organization, acl=acl)
|
||||||
user=user, organization=organization, acl=acl
|
|
||||||
)
|
|
||||||
|
|
||||||
deleted_org = await client.delete(
|
deleted_org = await client.delete(
|
||||||
f"https://localhost/api/v1/organizations/{organization.id}",
|
f"https://localhost/api/v1/organizations/{organization.id}",
|
||||||
|
|||||||
Reference in New Issue
Block a user