diff --git a/api/asset_manager/src/config.py b/api/asset_manager/src/config.py index 40b8e6a8..845266a6 100644 --- a/api/asset_manager/src/config.py +++ b/api/asset_manager/src/config.py @@ -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") diff --git a/api/asset_manager/src/database.py b/api/asset_manager/src/database.py index 5213f465..16934f48 100644 --- a/api/asset_manager/src/database.py +++ b/api/asset_manager/src/database.py @@ -8,6 +8,7 @@ modules: dict[str, Any] = { "modules.auth.models", "modules.users.models", "modules.organizations.models", + "modules.invitations.models", ] } diff --git a/api/asset_manager/src/migrations/models/0_20250625121149_init.py b/api/asset_manager/src/migrations/models/0_20250709154845_init.py similarity index 84% rename from api/asset_manager/src/migrations/models/0_20250625121149_init.py rename to api/asset_manager/src/migrations/models/0_20250709154845_init.py index a1585f4a..3f17e96c 100644 --- a/api/asset_manager/src/migrations/models/0_20250625121149_init.py +++ b/api/asset_manager/src/migrations/models/0_20250709154845_init.py @@ -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,56 @@ 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 ) /* 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, + "disabled" INT NOT NULL DEFAULT 0, + "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "disabled_at" TIMESTAMP +); CREATE TABLE IF NOT EXISTS "aerich" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "version" VARCHAR(255) NOT NULL, diff --git a/api/asset_manager/src/mixins/CMDMixin.py b/api/asset_manager/src/mixins/CMDMixin.py deleted file mode 100644 index 1d3b1b11..00000000 --- a/api/asset_manager/src/mixins/CMDMixin.py +++ /dev/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) - diff --git a/api/asset_manager/src/modules/auth/models.py b/api/asset_manager/src/modules/auth/models.py index a1c3b0cb..fe5fa3f5 100644 --- a/api/asset_manager/src/modules/auth/models.py +++ b/api/asset_manager/src/modules/auth/models.py @@ -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() - diff --git a/api/asset_manager/src/modules/invitations/models.py b/api/asset_manager/src/modules/invitations/models.py index 9e276916..582dfa33 100644 --- a/api/asset_manager/src/modules/invitations/models.py +++ b/api/asset_manager/src/modules/invitations/models.py @@ -1,15 +1,15 @@ - import uuid from tortoise import Model, fields -from mixins import CMDMixin -from modules.users.models import User - -class Invite(Model, CMDMixin): +class Invite(Model): id: uuid.UUID = fields.UUIDField(primary_key=True) receiver: str = fields.CharField(max_length=128) - sender: str = fields.UUIDField() + sender: uuid.UUID = fields.UUIDField() + org_id: uuid.UUID = fields.UUIDField() message: str | None = fields.TextField(null=True) accepted: bool = fields.BooleanField() 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) diff --git a/api/asset_manager/src/modules/invitations/router.py b/api/asset_manager/src/modules/invitations/router.py index b4c540bb..5cfd4c07 100644 --- a/api/asset_manager/src/modules/invitations/router.py +++ b/api/asset_manager/src/modules/invitations/router.py @@ -1,8 +1,10 @@ -from typing import Annotated +from typing import Annotated, List import uuid from fastapi import APIRouter, Depends, HTTPException from modules.invitations.models import Invite +from modules.invitations.schemas import invitation_model + from modules.users.models import User from modules.users.utils import get_current_active_user @@ -12,44 +14,63 @@ from tortoise.expressions import Q router = APIRouter(prefix="/api/v1/Users", tags=["User"]) -@router.get("/") -async def get_all_invitations(user: Annotated[User, Depends(get_current_active_user)]): - pass +@router.get("/", response_model=List[invitation_model]) +async def get_all_invitations( + user: Annotated[User, Depends(get_current_active_user)], +) -> List[Invite]: + invites: List[Invite] | None = await Invite.filter( + Q(receiver=user.username) | Q(receiver=user.email) + ) + + return invites -@router.get("/accept/{invitation_id}") +@router.get("/accept/{invitation_id}", response_model=invitation_model) async def accept_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 = True - invite.save() - return invite + return await invite.save() - -@router.get("/reject/{invitation_id}") +@router.get("/reject/{invitation_id}", response_model=invitation_model) 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 - invite.save() - return invite + return await invite.save() @router.get("/send") -async def accept_invitation( +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}", status_code=204) +@router.get("/cancel/{invitation_id}", response_model=invitation_model) async def accept_invitation( user: Annotated[User, Depends(get_current_active_user)], invitation_id: uuid.UUID ) -> None: @@ -63,4 +84,4 @@ async def accept_invitation( detail="The invitation doesn't exist or you don't have access to it.", ) - invite.delete() + return await invite.delete() diff --git a/api/asset_manager/src/modules/invitations/schemas.py b/api/asset_manager/src/modules/invitations/schemas.py index e69de29b..2ef3455c 100644 --- a/api/asset_manager/src/modules/invitations/schemas.py +++ b/api/asset_manager/src/modules/invitations/schemas.py @@ -0,0 +1,7 @@ + +from tortoise.contrib.pydantic import pydantic_model_creator + +from modules.invitations.models import Invite + +invitation_model = pydantic_model_creator(Invite) + diff --git a/api/asset_manager/src/modules/organizations/models.py b/api/asset_manager/src/modules/organizations/models.py index 970ea68b..306d6194 100644 --- a/api/asset_manager/src/modules/organizations/models.py +++ b/api/asset_manager/src/modules/organizations/models.py @@ -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() - - diff --git a/api/asset_manager/src/modules/organizations/router.py b/api/asset_manager/src/modules/organizations/router.py index 07922520..ba4be69a 100644 --- a/api/asset_manager/src/modules/organizations/router.py +++ b/api/asset_manager/src/modules/organizations/router.py @@ -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 diff --git a/api/asset_manager/src/modules/users/models.py b/api/asset_manager/src/modules/users/models.py index 6afb9dd5..c7d26362 100644 --- a/api/asset_manager/src/modules/users/models.py +++ b/api/asset_manager/src/modules/users/models.py @@ -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 @@ -102,6 +103,9 @@ class Membership(Model, CMDMixin): user: User = fields.ForeignKeyField("models.User") 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: diff --git a/api/asset_manager/src/requirements/requirements.txt b/api/asset_manager/src/requirements/requirements.txt index dd831c37..917f1342 100644 --- a/api/asset_manager/src/requirements/requirements.txt +++ b/api/asset_manager/src/requirements/requirements.txt @@ -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