Setup infrastructure for first patch
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
class Settings:
|
||||
PROJECT_NAME:str = "Open Asset Manager"
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
import pytz
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "Open Asset Manager"
|
||||
PROJECT_VERSION: str = "0.0.1"
|
||||
PROJECT_SUMMARY: str = "Product API for Open Asset Manager."
|
||||
SECRET_KEY: str | None = None
|
||||
HASHING_SCHEME: str = "HS256"
|
||||
PSQL_CONNECT_STR: str | None = None
|
||||
ACCESS_TOKEN_EXPIRE_MIN: int = 30
|
||||
REFRESH_TOKEN_EXPIRE_MIN: int = 60
|
||||
DEFAULT_TIMEZONE = pytz.UTC
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from typing_extensions import Any
|
||||
from tortoise import Tortoise
|
||||
import os
|
||||
from config import settings
|
||||
|
||||
db_url = os.getenv('PSQL_CONNECT_STR')
|
||||
modules: dict[str, Any] = {'models': [
|
||||
'.models',
|
||||
'.modules.auth.models',
|
||||
'.modules.assets.models',
|
||||
]}
|
||||
db_url = settings.PSQL_CONNECT_STR
|
||||
modules: dict[str, Any] = {
|
||||
"models": [
|
||||
".models",
|
||||
".modules.auth.models",
|
||||
".modules.assets.models",
|
||||
]
|
||||
}
|
||||
|
||||
TORTOISE_ORM = {
|
||||
"connections": {"default": db_url},
|
||||
@@ -19,11 +21,10 @@ TORTOISE_ORM = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def init_db():
|
||||
await Tortoise.init(
|
||||
db_url=db_url,
|
||||
modules=modules
|
||||
)
|
||||
await Tortoise.init(db_url=db_url, modules=modules)
|
||||
|
||||
|
||||
async def migrate_db():
|
||||
await init_db()
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
|
||||
from fastapi import FastAPI
|
||||
from starlette.responses import RedirectResponse
|
||||
from .config import settings
|
||||
from .modules.assets.router import router as asset_router
|
||||
from dotenv import load_dotenv
|
||||
from src.config import settings
|
||||
from modules.assets.router import router as asset_router
|
||||
from tortoise.contrib.fastapi import register_tortoise
|
||||
from .database import db_url, modules
|
||||
|
||||
load_dotenv()
|
||||
from src.database import db_url, modules
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
@@ -25,6 +21,7 @@ register_tortoise(
|
||||
|
||||
app.include_router(asset_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def main():
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
+177
-35
@@ -1,13 +1,21 @@
|
||||
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 passlib.context import CryptContext # type: ignore
|
||||
|
||||
from src.config import settings
|
||||
|
||||
crypt = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
class EnumField(fields.CharField):
|
||||
"""
|
||||
An example extension to CharField that serializes Enums
|
||||
to and from a str representation in the DB.
|
||||
Serializes Enums to and from a str representation in the DB.
|
||||
"""
|
||||
|
||||
def __init__(self, enum_type: Type[Enum], **kwargs):
|
||||
@@ -24,51 +32,185 @@ class EnumField(fields.CharField):
|
||||
return self._enum_type(value)
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
"Database value {} does not exist on Enum {}.".format(value, self._enum_type)
|
||||
"Database value {} does not exist on Enum {}.".format(
|
||||
value, self._enum_type
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class OrganizationType(Enum):
|
||||
HOME = 1 # Home use (Any size)
|
||||
SMALL_ORGANIZATION = 2 # 1-100
|
||||
MEDIUM_ORGANIZATION = 3 # 100 - 500
|
||||
LARGE_ORGANIZATION = 4 # 500 - 1000
|
||||
EXTRA_LARGE_ORGANIZATION = 5 # 1000 - 5000+
|
||||
"""
|
||||
Represents the following:
|
||||
|
||||
class CreatedAndModifiedMixin():
|
||||
created = fields.DatetimeField(null=True, auto_now_add=True)
|
||||
modified = fields.DatetimeField(null=True, auto_now=True)
|
||||
1. Is this a commercial entity or not?
|
||||
2. What size is it?
|
||||
|
||||
class Organization(Model, CreatedAndModifiedMixin):
|
||||
id = fields.UUIDField(primary_key=True)
|
||||
name = fields.CharField(max_length=128)
|
||||
type = EnumField(OrganizationType)
|
||||
users = fields.ManyToManyField('models.User',
|
||||
related_name='members',
|
||||
through="Membership",
|
||||
forward_key='user_id',
|
||||
backward_key='organization_id',
|
||||
null=True
|
||||
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:
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
class User(Model, CreatedAndModifiedMixin):
|
||||
id = fields.UUIDField(primary_key=True)
|
||||
name = fields.TextField()
|
||||
surname = fields.TextField()
|
||||
password = fields.CharField(max_length=128, null=True)
|
||||
organizations = fields.ManyToManyField('models.Organization',
|
||||
related_name='members',
|
||||
through='Membership',
|
||||
forward_key='organization_id',
|
||||
backward_key='user_id',
|
||||
null=True
|
||||
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}"
|
||||
|
||||
class Membership(Model, CreatedAndModifiedMixin):
|
||||
organization = fields.ForeignKeyField('models.Organization')
|
||||
user = fields.ForeignKeyField('models.User')
|
||||
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,33 @@
|
||||
import os
|
||||
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from fastapi.routing import APIRouter
|
||||
from src.modules.auth.schemas import UserModel
|
||||
from src.models import User
|
||||
from fastapi import HTTPException
|
||||
from src.config import settings
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
error: str = "E-Mail Address or password is incorrect"
|
||||
|
||||
@router.post("/")
|
||||
async def login(email: str, password: str):
|
||||
user: User = await User.get_or_none(email=email)
|
||||
if user is None:
|
||||
HTTPException(status_code=401, detail=error)
|
||||
if user.check_against_password(password) is False:
|
||||
HTTPException(status_code=401, detail=error)
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_login():
|
||||
pass
|
||||
|
||||
@router.post("/register")
|
||||
async def register():
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
|
||||
from src.models import Organization, User
|
||||
|
||||
OrganizationModel = pydantic_model_creator(Organization)
|
||||
|
||||
UserModel = pydantic_model_creator(User)
|
||||
@@ -1,7 +1,9 @@
|
||||
aerich==0.8.0
|
||||
fastapi[standard]==0.115.5
|
||||
uvicorn==0.31.1
|
||||
pydantic-settings==2.7.0
|
||||
tortoise-orm[asyncpg]==0.22.1
|
||||
uvicorn==0.31.1
|
||||
black==24.10.0
|
||||
python-dotenv==1.0.1
|
||||
uvicorn[standard]==0.34.0
|
||||
authlib==1.3.2
|
||||
passlib==1.7.4
|
||||
pytz==2024.2
|
||||
Reference in New Issue
Block a user