Invitations #11
@@ -1,8 +1,6 @@
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
|
||||
from passlib.context import CryptContext # type: ignore
|
||||
import pytz
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pwdlib import PasswordHash
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "StoneEdge Asset Management System"
|
||||
PROJECT_VERSION: str = "0.0.1"
|
||||
@@ -19,7 +17,7 @@ class Settings(BaseSettings):
|
||||
ACCESS_TOKEN_EXPIRE_MIN: int = 10
|
||||
REFRESH_TOKEN_EXPIRE_MIN: int = 20
|
||||
BACKEND_CORS_ORIGINS: list = ["*"]
|
||||
CRYPT: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
CRYPT: PasswordHash = PasswordHash.recommended()
|
||||
OAUTH2_SCHEME: OAuth2PasswordBearer = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
@@ -8,6 +8,7 @@ modules: dict[str, Any] = {
|
||||
"modules.auth.models",
|
||||
"modules.users.models",
|
||||
"modules.organizations.models",
|
||||
"modules.invitations.models",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from modules.assets.router import router as asset_router
|
||||
from modules.auth.router import router as auth_router
|
||||
from modules.users.router import router as users_router
|
||||
from modules.organizations.router import router as organizations_router
|
||||
from modules.invitations.router import router as invitations_router
|
||||
|
||||
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
@@ -53,3 +54,4 @@ app.include_router(auth_router)
|
||||
app.include_router(users_router)
|
||||
app.include_router(organizations_router)
|
||||
app.include_router(asset_router)
|
||||
app.include_router(invitations_router)
|
||||
|
||||
+29
-16
@@ -12,9 +12,6 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
"ADMIN" INT NOT NULL DEFAULT 0
|
||||
) /* ACL */;
|
||||
CREATE TABLE IF NOT EXISTS "organization" (
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||
"name" VARCHAR(128) NOT NULL,
|
||||
"type" VARCHAR(128) NOT NULL,
|
||||
@@ -23,41 +20,57 @@ CREATE TABLE IF NOT EXISTS "organization" (
|
||||
"state" VARCHAR(128),
|
||||
"city" VARCHAR(128),
|
||||
"country" VARCHAR(128),
|
||||
"disabled" INT NOT NULL DEFAULT 0
|
||||
) /* Organization */;
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"disabled" INT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP
|
||||
) /* Organization */;
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||
"email" VARCHAR(128) NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"surname" TEXT NOT NULL,
|
||||
"password" VARCHAR(128),
|
||||
"disabled" INT NOT NULL DEFAULT 0
|
||||
) /* User */;
|
||||
CREATE TABLE IF NOT EXISTS "token" (
|
||||
"disabled" INT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP
|
||||
) /* User */;
|
||||
CREATE TABLE IF NOT EXISTS "token" (
|
||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||
"token_type" VARCHAR(128) NOT NULL DEFAULT 'Bearer',
|
||||
"access_token" TEXT,
|
||||
"refresh_token" TEXT,
|
||||
"disabled" INT NOT NULL DEFAULT 0,
|
||||
"user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||
) /* Token */;
|
||||
CREATE TABLE IF NOT EXISTS "membership" (
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||
) /* Token */;
|
||||
CREATE TABLE IF NOT EXISTS "membership" (
|
||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||
"disabled" INT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE,
|
||||
"organization_id" CHAR(36) NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE,
|
||||
"user_id" CHAR(36) NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
|
||||
"organization_id" CHAR(36) REFERENCES "organization" ("id") ON DELETE SET NULL,
|
||||
"user_id" CHAR(36) REFERENCES "user" ("id") ON DELETE SET NULL
|
||||
) /* Membership */;
|
||||
CREATE TABLE IF NOT EXISTS "invite" (
|
||||
"id" CHAR(36) NOT NULL PRIMARY KEY,
|
||||
"receiver" VARCHAR(128) NOT NULL,
|
||||
"sender" CHAR(36) NOT NULL,
|
||||
"org_id" CHAR(36) NOT NULL,
|
||||
"message" TEXT,
|
||||
"accepted" INT NOT NULL DEFAULT 0,
|
||||
"disabled" INT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
"disabled_at" TIMESTAMP,
|
||||
"acl_id" CHAR(36) NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"version" VARCHAR(255) NOT NULL,
|
||||
@@ -1,11 +0,0 @@
|
||||
from tortoise import fields
|
||||
|
||||
class CMDMixin():
|
||||
"""
|
||||
Created, modified and delete mixin, these are required for every class.
|
||||
"""
|
||||
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||
disabled_at = fields.DatetimeField(null=True)
|
||||
|
||||
@@ -4,11 +4,8 @@ from tortoise import fields
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from mixins.CMDMixin import CMDMixin
|
||||
from config import settings
|
||||
|
||||
|
||||
class Token(Model, CMDMixin):
|
||||
class Token(Model):
|
||||
"""
|
||||
Token
|
||||
|
||||
@@ -21,9 +18,11 @@ class Token(Model, CMDMixin):
|
||||
access_token: str = fields.TextField(null=True)
|
||||
refresh_token: str = fields.TextField(null=True)
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||
disabled_at = fields.DatetimeField(null=True)
|
||||
|
||||
async def delete(self) -> None:
|
||||
self.disabled = True
|
||||
self.disabled_at = datetime.now(tz=pytz.UTC)
|
||||
await self.save()
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
import uuid
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi.routing import APIRouter
|
||||
import pytz
|
||||
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.models import Token
|
||||
from modules.users.models import User
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from tortoise.expressions import Q
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
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:
|
||||
raise HTTPException(status_code=401, detail=account_error)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=account_error
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)]):
|
||||
"""
|
||||
Logout
|
||||
@@ -110,7 +117,9 @@ async def refresh_login(
|
||||
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):
|
||||
# Prevent existing users from reapplying for our system.
|
||||
existing_user: User | None = await User.filter(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import pytz
|
||||
from tortoise import Model, fields
|
||||
|
||||
from modules.users.models import ACL
|
||||
|
||||
|
||||
class Invite(Model):
|
||||
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
||||
receiver: str = fields.CharField(max_length=128)
|
||||
sender: uuid.UUID = fields.UUIDField()
|
||||
org_id: uuid.UUID = fields.UUIDField()
|
||||
message: str | None = fields.TextField(null=True)
|
||||
acl: ACL = fields.ForeignKeyField("models.ACL")
|
||||
accepted: bool = fields.BooleanField(default=False)
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=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()
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
from typing import Annotated, List
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from modules.organizations.models import Organization
|
||||
from modules.invitations.models import Invite
|
||||
from modules.invitations.schemas import invitation_model, send_invitation_for_org
|
||||
|
||||
from modules.users.models import ACL, Membership, User
|
||||
from modules.users.utils import get_current_active_user
|
||||
|
||||
from tortoise.expressions import Q
|
||||
|
||||
router = APIRouter(prefix="/api/v1/invitations", tags=["invites"])
|
||||
|
||||
|
||||
@router.get("/", response_model=List[invitation_model])
|
||||
async def get_all_invitations(
|
||||
user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> List[Invite]:
|
||||
"""Returns all invitations for user requesting, except disabled invites.
|
||||
|
||||
Args:
|
||||
user (Annotated[User, Depends(get_current_active_user)]): Returns user token.
|
||||
|
||||
Returns:
|
||||
List[Invite]: A list of invitations.
|
||||
"""
|
||||
return await Invite.filter(
|
||||
(Q(sender=user.id) | (Q(receiver=user.username) | Q(receiver=user.email)))
|
||||
& Q(disabled=False)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{invitation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_invitation(
|
||||
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
||||
) -> 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(
|
||||
Q(id=invitation_id) & Q(sender=user.id) & Q(disabled=False)
|
||||
)
|
||||
|
||||
if not invite:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="The invitation doesn't exist or you don't have access to it.",
|
||||
)
|
||||
|
||||
await invite.delete()
|
||||
|
||||
|
||||
@router.get("/accept/{invitation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def accept_invitation(
|
||||
user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID
|
||||
) -> 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(
|
||||
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.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,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
|
||||
from modules.invitations.models import 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
|
||||
@@ -7,7 +7,6 @@ from tortoise.exceptions import ConfigurationError
|
||||
from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
from mixins.CMDMixin import CMDMixin
|
||||
|
||||
class EnumField(fields.CharField):
|
||||
"""
|
||||
@@ -52,8 +51,7 @@ class OrganizationType(Enum):
|
||||
EXTRA_LARGE_ORGANIZATION: str = "xl_org" # 1000 - 5000+
|
||||
|
||||
|
||||
|
||||
class Organization(Model, CMDMixin):
|
||||
class Organization(Model):
|
||||
"""
|
||||
Organization
|
||||
|
||||
@@ -79,6 +77,9 @@ class Organization(Model, CMDMixin):
|
||||
on_delete=fields.NO_ACTION,
|
||||
)
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||
disabled_at = fields.DatetimeField(null=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.id} - {self.name}"
|
||||
@@ -90,5 +91,3 @@ class Organization(Model, CMDMixin):
|
||||
self.disabled = True
|
||||
self.disabled_at = datetime.now(tz=pytz.UTC)
|
||||
await self.save()
|
||||
|
||||
|
||||
|
||||
@@ -20,11 +20,10 @@ router = APIRouter(prefix="/api/v1/organizations", tags=["orgs"])
|
||||
async def all_active_organizations(
|
||||
user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> List[Organization]:
|
||||
memberships: List[Membership] = list(
|
||||
await Membership.filter(
|
||||
Q(user_id=user.id) & Q(disabled=False)
|
||||
).prefetch_related("organization")
|
||||
)
|
||||
memberships: List[Membership] = await Membership.filter(
|
||||
Q(user_id=user.id) & Q(disabled=False)
|
||||
).prefetch_related("organization")
|
||||
|
||||
organizations: List[Organization] = []
|
||||
|
||||
if len(memberships) < 1:
|
||||
@@ -59,7 +58,7 @@ async def delete_organization(
|
||||
for member in all_memberships:
|
||||
await member.acl.delete()
|
||||
await member.delete()
|
||||
|
||||
|
||||
await membership.acl.delete()
|
||||
await membership.delete()
|
||||
return
|
||||
|
||||
@@ -6,13 +6,12 @@ from tortoise.models import Model
|
||||
from tortoise import fields
|
||||
|
||||
from modules.organizations.models import Organization
|
||||
from mixins.CMDMixin import CMDMixin
|
||||
from config import settings
|
||||
|
||||
crypt = settings.CRYPT
|
||||
|
||||
|
||||
class User(Model, CMDMixin):
|
||||
class User(Model):
|
||||
"""
|
||||
User
|
||||
|
||||
@@ -35,7 +34,9 @@ class User(Model, CMDMixin):
|
||||
on_delete=fields.NO_ACTION,
|
||||
)
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
# tokens = fields.ForeignKeyField("models.Token")
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||
disabled_at = fields.DatetimeField(null=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.id} - {self.name} {self.surname}"
|
||||
@@ -90,7 +91,7 @@ class ACL(Model):
|
||||
"""
|
||||
|
||||
|
||||
class Membership(Model, CMDMixin):
|
||||
class Membership(Model):
|
||||
"""
|
||||
Membership
|
||||
|
||||
@@ -98,10 +99,13 @@ class Membership(Model, CMDMixin):
|
||||
"""
|
||||
|
||||
id: uuid.UUID = fields.UUIDField(primary_key=True)
|
||||
organization: Organization = fields.ForeignKeyField("models.Organization")
|
||||
user: User = fields.ForeignKeyField("models.User")
|
||||
organization: Organization = fields.ForeignKeyField("models.Organization", null=True, on_delete=fields.SET_NULL)
|
||||
user: User = fields.ForeignKeyField("models.User", null=True, on_delete=fields.SET_NULL)
|
||||
acl: ACL = fields.ForeignKeyField("models.ACL")
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
created_at = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified_at = fields.DatetimeField(null=True, auto_now=True)
|
||||
disabled_at = fields.DatetimeField(null=True)
|
||||
|
||||
async def delete(self, force: bool = False) -> None:
|
||||
if force:
|
||||
|
||||
@@ -4,7 +4,7 @@ tortoise-orm[asyncpg]>=0.25.1
|
||||
uvicorn>=0.34.3
|
||||
black>=25.1.0
|
||||
joserfc>=1.1.0
|
||||
passlib>=1.7.4
|
||||
pwdlib[argon2]>=0.2.1
|
||||
pytz>=2025.2
|
||||
ptpython>=3.0.30
|
||||
msgspec>=0.19.0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from tests.base_test import Test
|
||||
from modules.users.models import User
|
||||
import pytest # type: ignore
|
||||
from httpx import AsyncClient
|
||||
from config import settings
|
||||
from unittest.mock import ANY
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
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_removing_invitations(
|
||||
self, client: AsyncClient, create_user_with_org
|
||||
):
|
||||
admin, org, _, admintokens = await create_user_with_org(
|
||||
email="superadmin9999@localhost.com",
|
||||
username="awesomeadmin",
|
||||
password="awesomeadmin",
|
||||
is_admin=True,
|
||||
)
|
||||
|
||||
invite = await client.post(
|
||||
"https://localhost/api/v1/invitations/send",
|
||||
json={
|
||||
"org_id": str(org.id),
|
||||
"receiver": "user9487@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": "user9487@localhost.com",
|
||||
"sender": str(admin.id),
|
||||
}
|
||||
|
||||
invite_id = invite.json()["id"]
|
||||
removed_invite = await client.delete(
|
||||
f"https://localhost/api/v1/invitations/{invite_id}",
|
||||
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||
)
|
||||
|
||||
assert removed_invite.status_code == 204
|
||||
|
||||
|
||||
async def test_cannot_accept_own_invite(
|
||||
self, client: AsyncClient, create_user_with_org
|
||||
):
|
||||
admin, org, _, admintokens = await create_user_with_org(
|
||||
email="superadmin18569@localhost.com",
|
||||
username="awesomeadmin",
|
||||
password="awesomeadmin",
|
||||
is_admin=True,
|
||||
)
|
||||
|
||||
invite = await client.post(
|
||||
"https://localhost/api/v1/invitations/send",
|
||||
json={
|
||||
"org_id": str(org.id),
|
||||
"receiver": "non-existing-user@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": "non-existing-user@localhost.com",
|
||||
"sender": str(admin.id),
|
||||
}
|
||||
|
||||
invite_id = invite.json()["id"]
|
||||
try_accept_invite = await client.get(
|
||||
f"https://localhost/api/v1/invitations/accept/{invite_id}",
|
||||
headers={"Authorization": f"Bearer {admintokens.access_token}"},
|
||||
)
|
||||
|
||||
assert try_accept_invite.status_code == 403
|
||||
assert try_accept_invite.json() == {
|
||||
"detail": "The invitation doesn't exist or you don't have access to it."
|
||||
}
|
||||
|
||||
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 modules.users.models import ACL, Membership
|
||||
from modules.organizations.models import Organization
|
||||
from config import settings
|
||||
from unittest.mock import ANY
|
||||
from tests.base_test import Test
|
||||
|
||||
crypt = settings.CRYPT
|
||||
|
||||
|
||||
class TestOrganizationRoute(Test):
|
||||
async def test_get_organizations_from_api(
|
||||
@@ -153,9 +150,7 @@ class TestOrganizationRoute(Test):
|
||||
READ=True, WRITE=True, REPORT=True, MANAGE=False, ADMIN=False
|
||||
)
|
||||
|
||||
await Membership.create(
|
||||
user=user, organization=organization, acl=acl
|
||||
)
|
||||
await Membership.create(user=user, organization=organization, acl=acl)
|
||||
|
||||
deleted_org = await client.delete(
|
||||
f"https://localhost/api/v1/organizations/{organization.id}",
|
||||
|
||||
Reference in New Issue
Block a user