Move some items, fix some programming issues, start work on logging in
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
|
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
|
||||||
|
from passlib.context import CryptContext # type: ignore
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@@ -7,11 +8,12 @@ class Settings(BaseSettings):
|
|||||||
PROJECT_VERSION: str = "0.0.1"
|
PROJECT_VERSION: str = "0.0.1"
|
||||||
PROJECT_SUMMARY: str = "Product API for Open Asset Manager."
|
PROJECT_SUMMARY: str = "Product API for Open Asset Manager."
|
||||||
SECRET_KEY: str | None = None
|
SECRET_KEY: str | None = None
|
||||||
HASHING_SCHEME: str = "HS256"
|
HASHING_SCHEME: str = "HS512"
|
||||||
PSQL_CONNECT_STR: str | None = None
|
PSQL_CONNECT_STR: str | None = None
|
||||||
ACCESS_TOKEN_EXPIRE_MIN: int = 30
|
ACCESS_TOKEN_EXPIRE_MIN: int = 30
|
||||||
REFRESH_TOKEN_EXPIRE_MIN: int = 60
|
REFRESH_TOKEN_EXPIRE_MIN: int = 60
|
||||||
DEFAULT_TIMEZONE: str = pytz.UTC._tzname
|
DEFAULT_TIMEZONE: str = pytz.UTC._tzname
|
||||||
|
CRYPT: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
model_config = SettingsConfigDict(env_file=".env")
|
model_config = SettingsConfigDict(env_file=".env")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing_extensions import Any
|
from typing_extensions import Any
|
||||||
from tortoise import Tortoise
|
from tortoise import Tortoise
|
||||||
from src.config import settings
|
from config import settings
|
||||||
|
|
||||||
db_url = settings.PSQL_CONNECT_STR
|
db_url = settings.PSQL_CONNECT_STR
|
||||||
modules: dict[str, Any] = {
|
modules: dict[str, Any] = {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
from src.config import settings
|
from config import settings
|
||||||
from src.modules.assets.router import router as asset_router
|
from modules.assets.router import router as asset_router
|
||||||
from tortoise.contrib.fastapi import register_tortoise
|
from tortoise.contrib.fastapi import register_tortoise
|
||||||
from src.database import db_url, modules
|
from database import db_url, modules
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.PROJECT_NAME,
|
title=settings.PROJECT_NAME,
|
||||||
@@ -24,3 +25,7 @@ app.include_router(asset_router)
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def main():
|
async def main():
|
||||||
return RedirectResponse(url="/docs")
|
return RedirectResponse(url="/docs")
|
||||||
|
|
||||||
|
@app.get("/ping")
|
||||||
|
async def ping() -> JSONResponse:
|
||||||
|
return JSONResponse("PONG")
|
||||||
@@ -1,59 +1,4 @@
|
|||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Type
|
|
||||||
import uuid
|
|
||||||
from pydantic import EmailStr
|
|
||||||
from tortoise.exceptions import ConfigurationError
|
|
||||||
from tortoise.models import Model
|
|
||||||
from tortoise import fields
|
from tortoise import fields
|
||||||
from passlib.context import CryptContext # type: ignore
|
|
||||||
|
|
||||||
from src.config import settings
|
|
||||||
|
|
||||||
crypt = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
|
|
||||||
class EnumField(fields.CharField):
|
|
||||||
"""
|
|
||||||
Serializes Enums to and from a str representation in the DB.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, enum_type: Type[Enum], **kwargs):
|
|
||||||
super().__init__(128, **kwargs)
|
|
||||||
if not issubclass(enum_type, Enum):
|
|
||||||
raise ConfigurationError("{} is not a subclass of Enum!".format(enum_type))
|
|
||||||
self._enum_type = enum_type
|
|
||||||
|
|
||||||
def to_db_value(self, value: Enum, instance) -> str:
|
|
||||||
return value.value
|
|
||||||
|
|
||||||
def to_python_value(self, value: str) -> Enum:
|
|
||||||
try:
|
|
||||||
return self._enum_type(value)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(
|
|
||||||
"Database value {} does not exist on Enum {}.".format(
|
|
||||||
value, self._enum_type
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationType(Enum):
|
|
||||||
"""
|
|
||||||
Represents the following:
|
|
||||||
|
|
||||||
1. Is this a commercial entity or not?
|
|
||||||
2. What size is it?
|
|
||||||
|
|
||||||
All choices should be representative of the org.
|
|
||||||
There are no seat costs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
HOME: int = 1 # Home use (Any size)
|
|
||||||
SMALL_ORGANIZATION: int = 2 # 1-100
|
|
||||||
MEDIUM_ORGANIZATION: int = 3 # 100 - 500
|
|
||||||
LARGE_ORGANIZATION: int = 4 # 500 - 1000
|
|
||||||
EXTRA_LARGE_ORGANIZATION: int = 5 # 1000 - 5000+
|
|
||||||
|
|
||||||
|
|
||||||
class CMDMixin:
|
class CMDMixin:
|
||||||
@@ -64,153 +9,3 @@ class CMDMixin:
|
|||||||
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)
|
||||||
|
|
||||||
|
|
||||||
class Organization(Model, CMDMixin):
|
|
||||||
"""
|
|
||||||
Organization
|
|
||||||
|
|
||||||
This class holds the organization for a household / organization
|
|
||||||
and makes sure that we can add users.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: uuid = fields.UUIDField(primary_key=True)
|
|
||||||
name: str = fields.CharField(max_length=128)
|
|
||||||
type: str = EnumField(OrganizationType)
|
|
||||||
users: uuid = fields.ManyToManyField(
|
|
||||||
"models.User",
|
|
||||||
related_name="members",
|
|
||||||
through="Membership",
|
|
||||||
forward_key="user_id",
|
|
||||||
backward_key="organization_id",
|
|
||||||
null=True,
|
|
||||||
on_delete=fields.NO_ACTION,
|
|
||||||
)
|
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.id} - {self.name}"
|
|
||||||
|
|
||||||
def delete(self) -> None:
|
|
||||||
self.disabled = True
|
|
||||||
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Token(Model, CMDMixin):
|
|
||||||
"""
|
|
||||||
Token
|
|
||||||
|
|
||||||
Creates the access tokens for the User
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: uuid = fields.UUIDField(primary_key=True)
|
|
||||||
user: uuid = fields.ForeignKeyField("models.User")
|
|
||||||
token_type: str = fields.CharField(max_length=128, default="bearer")
|
|
||||||
access_token: str = fields.CharField(max_length=128, null=True)
|
|
||||||
refresh_token: str = fields.CharField(max_length=128, null=True)
|
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
|
||||||
|
|
||||||
def delete(self) -> None:
|
|
||||||
self.disabled = True
|
|
||||||
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
class User(Model, CMDMixin):
|
|
||||||
"""
|
|
||||||
User
|
|
||||||
|
|
||||||
This holds all of our users
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: uuid = fields.UUIDField(primary_key=True)
|
|
||||||
email: EmailStr = fields.CharField(max_length=128)
|
|
||||||
username: str = fields.TextField(max_length=128)
|
|
||||||
name: str = fields.TextField(max_length=128)
|
|
||||||
surname: str = fields.TextField(max_length=128)
|
|
||||||
password: str = fields.CharField(max_length=128, null=True)
|
|
||||||
organizations: uuid = fields.ManyToManyField(
|
|
||||||
"models.Organization",
|
|
||||||
related_name="members",
|
|
||||||
through="Membership",
|
|
||||||
forward_key="organization_id",
|
|
||||||
backward_key="user_id",
|
|
||||||
null=True,
|
|
||||||
on_delete=fields.NO_ACTION,
|
|
||||||
)
|
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
|
||||||
tokens = fields.ForeignKeyField("models.Token")
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.id} - {self.name} {self.surname}"
|
|
||||||
|
|
||||||
def set_password(self, password: str) -> None:
|
|
||||||
secret_key = settings.SECRET_KEY
|
|
||||||
if secret_key is None:
|
|
||||||
return False
|
|
||||||
self.password = crypt.hash(
|
|
||||||
password,
|
|
||||||
secret=secret_key,
|
|
||||||
scheme=settings.HASHING_SCHEME,
|
|
||||||
)
|
|
||||||
self.save() # Make sure to save the model in DB
|
|
||||||
|
|
||||||
def check_against_password(self, password: str) -> bool:
|
|
||||||
secret_key = settings.SECRET_KEY
|
|
||||||
if secret_key is None:
|
|
||||||
return False
|
|
||||||
return crypt.verify(
|
|
||||||
password,
|
|
||||||
secret=secret_key,
|
|
||||||
scheme=settings.HASHING_SCHEME,
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self) -> None:
|
|
||||||
self.disabled = True
|
|
||||||
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
class ACL(Model):
|
|
||||||
"""
|
|
||||||
ACL
|
|
||||||
|
|
||||||
Access control lists, every invited user gets an ACL and this decides whether you grant / deny access to certain parts of our system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: uuid = fields.UUIDField(primary_key=True)
|
|
||||||
READ: bool = fields.BooleanField(default=False)
|
|
||||||
WRITE: bool = fields.BooleanField(default=False)
|
|
||||||
REPORT: bool = fields.BooleanField(default=False)
|
|
||||||
MANAGE: bool = fields.BooleanField(default=False)
|
|
||||||
ADMIN: bool = fields.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"""
|
|
||||||
ID: {self.id},
|
|
||||||
READ: {self.READ},
|
|
||||||
WRITE: {self.WRITE},
|
|
||||||
REPORT: {self.REPORT},
|
|
||||||
MANAGE: {self.MANAGE},
|
|
||||||
ADMIN: {self.ADMIN}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Membership(Model, CMDMixin):
|
|
||||||
"""
|
|
||||||
Membership
|
|
||||||
|
|
||||||
Creates a connection between an user and a company together with an ACL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: uuid = fields.UUIDField(primary_key=True)
|
|
||||||
organization: Organization = fields.ForeignKeyField("models.Organization")
|
|
||||||
user: User = fields.ForeignKeyField("models.User")
|
|
||||||
acl: ACL = fields.ForeignKeyField("models.ACL")
|
|
||||||
disabled: bool = fields.BooleanField(default=False)
|
|
||||||
|
|
||||||
def delete(self) -> None:
|
|
||||||
self.disabled = True
|
|
||||||
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
|
||||||
self.save()
|
|
||||||
|
|||||||
@@ -1 +1,28 @@
|
|||||||
|
from tortoise.models import Model
|
||||||
|
from tortoise import fields
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from models import CMDMixin
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
|
||||||
|
class Token(Model, CMDMixin):
|
||||||
|
"""
|
||||||
|
Token
|
||||||
|
|
||||||
|
Creates the access tokens for the User
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: uuid = fields.UUIDField(primary_key=True)
|
||||||
|
user: uuid = fields.ForeignKeyField("modules.users.models.User")
|
||||||
|
token_type: str = fields.CharField(max_length=128, default="bearer")
|
||||||
|
access_token: str = fields.CharField(max_length=128, null=True)
|
||||||
|
refresh_token: str = fields.CharField(max_length=128, null=True)
|
||||||
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.disabled = True
|
||||||
|
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import os
|
from typing import Annotated
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
|
||||||
|
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from src.modules.auth.schemas import UserModel
|
from modules.users.models import User
|
||||||
from src.models import User
|
from fastapi import Depends, HTTPException
|
||||||
from fastapi import HTTPException
|
from config import settings
|
||||||
from src.config import settings
|
from tortoise.expressions import Q
|
||||||
|
from authlib.jose import jwt # type: ignore
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth")
|
router = APIRouter(prefix="/auth")
|
||||||
|
|
||||||
@@ -16,13 +16,17 @@ ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MIN
|
|||||||
|
|
||||||
error: str = "E-Mail Address or password is incorrect"
|
error: str = "E-Mail Address or password is incorrect"
|
||||||
|
|
||||||
|
crypt = settings.CRYPT
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def login(email: str, password: str):
|
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
||||||
user: User = await User.get_or_none(email=email)
|
user: User = await User.filter(Q(email=form.username)).get_or_none()
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
HTTPException(status_code=401, detail=error)
|
HTTPException(status_code=401, detail=error)
|
||||||
if user.check_against_password(password) is False:
|
|
||||||
|
if user.check_against_password(form.password) is False:
|
||||||
HTTPException(status_code=401, detail=error)
|
HTTPException(status_code=401, detail=error)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Type
|
||||||
|
import uuid
|
||||||
|
from tortoise.exceptions import ConfigurationError
|
||||||
|
from tortoise.models import Model
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from models import CMDMixin
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
class EnumField(fields.CharField):
|
||||||
|
"""
|
||||||
|
Serializes Enums to and from a str representation in the DB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, enum_type: Type[Enum], **kwargs):
|
||||||
|
super().__init__(128, **kwargs)
|
||||||
|
if not issubclass(enum_type, Enum):
|
||||||
|
raise ConfigurationError("{} is not a subclass of Enum!".format(enum_type))
|
||||||
|
self._enum_type = enum_type
|
||||||
|
|
||||||
|
def to_db_value(self, value: Enum, instance) -> str:
|
||||||
|
return value.value
|
||||||
|
|
||||||
|
def to_python_value(self, value: str) -> Enum:
|
||||||
|
try:
|
||||||
|
return self._enum_type(value)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(
|
||||||
|
"Database value {} does not exist on Enum {}.".format(
|
||||||
|
value, self._enum_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationType(Enum):
|
||||||
|
"""
|
||||||
|
Represents the following:
|
||||||
|
|
||||||
|
1. Is this a commercial entity or not?
|
||||||
|
2. What size is it?
|
||||||
|
|
||||||
|
All choices should be representative of the org.
|
||||||
|
There are no seat costs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
HOME: int = 1 # Home use (Any size)
|
||||||
|
SMALL_ORGANIZATION: int = 2 # 1-100
|
||||||
|
MEDIUM_ORGANIZATION: int = 3 # 100 - 500
|
||||||
|
LARGE_ORGANIZATION: int = 4 # 500 - 1000
|
||||||
|
EXTRA_LARGE_ORGANIZATION: int = 5 # 1000 - 5000+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Organization(Model, CMDMixin):
|
||||||
|
"""
|
||||||
|
Organization
|
||||||
|
|
||||||
|
This class holds the organization for a household / organization
|
||||||
|
and makes sure that we can add users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: uuid = fields.UUIDField(primary_key=True)
|
||||||
|
name: str = fields.CharField(max_length=128)
|
||||||
|
type: str = EnumField(OrganizationType)
|
||||||
|
users: uuid = fields.ManyToManyField(
|
||||||
|
"models.User",
|
||||||
|
related_name="members",
|
||||||
|
through="Membership",
|
||||||
|
forward_key="user_id",
|
||||||
|
backward_key="organization_id",
|
||||||
|
null=True,
|
||||||
|
on_delete=fields.NO_ACTION,
|
||||||
|
)
|
||||||
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.id} - {self.name}"
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.disabled = True
|
||||||
|
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
from pydantic import EmailStr
|
||||||
|
from tortoise.models import Model
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from modules.organizations.models import Organization
|
||||||
|
from models import CMDMixin
|
||||||
|
from config import settings
|
||||||
|
|
||||||
|
crypt = settings.CRYPT
|
||||||
|
|
||||||
|
class User(Model, CMDMixin):
|
||||||
|
"""
|
||||||
|
User
|
||||||
|
|
||||||
|
This holds all of our users
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: uuid = fields.UUIDField(primary_key=True)
|
||||||
|
email: EmailStr = fields.CharField(max_length=128)
|
||||||
|
username: str = fields.TextField(max_length=128)
|
||||||
|
name: str = fields.TextField(max_length=128)
|
||||||
|
surname: str = fields.TextField(max_length=128)
|
||||||
|
password: str = fields.CharField(max_length=128, null=True)
|
||||||
|
organizations: uuid = fields.ManyToManyField(
|
||||||
|
"models.Organization",
|
||||||
|
related_name="members",
|
||||||
|
through="Membership",
|
||||||
|
forward_key="organization_id",
|
||||||
|
backward_key="user_id",
|
||||||
|
null=True,
|
||||||
|
on_delete=fields.NO_ACTION,
|
||||||
|
)
|
||||||
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
|
tokens = fields.ForeignKeyField("modules.auth.models.Token")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.id} - {self.name} {self.surname}"
|
||||||
|
|
||||||
|
def set_password(self, password: str) -> None:
|
||||||
|
self.password = crypt.hash(
|
||||||
|
password,
|
||||||
|
settings.HASHING_SCHEME
|
||||||
|
)
|
||||||
|
self.save() # Make sure to save the model in DB
|
||||||
|
|
||||||
|
def check_against_password(self, password: str) -> bool:
|
||||||
|
return crypt.verify(
|
||||||
|
password,
|
||||||
|
self.password,
|
||||||
|
settings.HASHING_SCHEME
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_password(self, old_password, new_password: str, verify_new_password: str) -> bool:
|
||||||
|
if self.check_against_password(old_password) is False:
|
||||||
|
return False
|
||||||
|
if new_password is not verify_new_password:
|
||||||
|
return False
|
||||||
|
self.set_password(new_password)
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.disabled = True
|
||||||
|
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ACL(Model):
|
||||||
|
"""
|
||||||
|
ACL
|
||||||
|
|
||||||
|
Access control lists, every invited user gets an ACL and this decides whether you grant / deny access to certain parts of our system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: uuid = fields.UUIDField(primary_key=True)
|
||||||
|
READ: bool = fields.BooleanField(default=False)
|
||||||
|
WRITE: bool = fields.BooleanField(default=False)
|
||||||
|
REPORT: bool = fields.BooleanField(default=False)
|
||||||
|
MANAGE: bool = fields.BooleanField(default=False)
|
||||||
|
ADMIN: bool = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"""
|
||||||
|
ID: {self.id},
|
||||||
|
READ: {self.READ},
|
||||||
|
WRITE: {self.WRITE},
|
||||||
|
REPORT: {self.REPORT},
|
||||||
|
MANAGE: {self.MANAGE},
|
||||||
|
ADMIN: {self.ADMIN}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Model, CMDMixin):
|
||||||
|
"""
|
||||||
|
Membership
|
||||||
|
|
||||||
|
Creates a connection between an user and a company together with an ACL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: uuid = fields.UUIDField(primary_key=True)
|
||||||
|
organization: Organization = fields.ForeignKeyField("models.Organization")
|
||||||
|
user: User = fields.ForeignKeyField("models.User")
|
||||||
|
acl: ACL = fields.ForeignKeyField("models.ACL")
|
||||||
|
disabled: bool = fields.BooleanField(default=False)
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
self.disabled = True
|
||||||
|
self.disabled_at = datetime.now(tz=settings.DEFAULT_TIMEZONE)
|
||||||
|
self.save()
|
||||||
Reference in New Issue
Block a user