Replace Passlib with drop-in replacement, add more invitation routes, start tests

This commit is contained in:
2025-07-09 16:34:42 +00:00
parent 9a01074ad1
commit b65c292d83
12 changed files with 101 additions and 72 deletions
+3 -5
View File
@@ -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")
+1
View File
@@ -8,6 +8,7 @@ modules: dict[str, Any] = {
"modules.auth.models",
"modules.users.models",
"modules.organizations.models",
"modules.invitations.models",
]
}
@@ -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,
-11
View File
@@ -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 -5
View File
@@ -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,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)
@@ -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()
@@ -0,0 +1,7 @@
from tortoise.contrib.pydantic import pydantic_model_creator
from modules.invitations.models import Invite
invitation_model = pydantic_model_creator(Invite)
@@ -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
@@ -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:
@@ -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