Setup database connection, fix docker-compose, setup aerich migrations and more

This commit is contained in:
2025-01-22 18:00:27 +02:00
parent 0f0b0f812a
commit cd89e5788f
17 changed files with 222 additions and 34 deletions
+7 -6
View File
@@ -1,15 +1,15 @@
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore from passlib.context import CryptContext # type: ignore
from passlib.context import CryptContext # type: ignore
import pytz import pytz
class Settings(BaseSettings): class Settings(BaseSettings):
PROJECT_NAME: str = "Open Asset Manager" PROJECT_NAME: str = "StoneEdge Asset Management System"
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 StoneEdge."
SECRET_KEY: str | None = None SECRET_KEY: str | None = None
HASHING_SCHEME: str = "HS512" HASHING_SCHEME: str = "HS512"
PSQL_CONNECT_STR: str | None = None PSQL_CONNECT_STR: str = "postgres://user:password@localhost:5432/stoneedge"
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
@@ -17,4 +17,5 @@ class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env") model_config = SettingsConfigDict(env_file=".env")
settings = Settings() settings = Settings()
+7 -6
View File
@@ -2,17 +2,18 @@ from typing_extensions import Any
from tortoise import Tortoise from tortoise import Tortoise
from config import settings from config import settings
db_url = settings.PSQL_CONNECT_STR
modules: dict[str, Any] = { modules: dict[str, Any] = {
"models": [ "models": [
".models", "models",
".modules.auth.models", "modules.assets.models",
".modules.assets.models", "modules.auth.models",
"modules.users.models",
"modules.organizations.models",
] ]
} }
TORTOISE_ORM = { TORTOISE_ORM = {
"connections": {"default": db_url}, "connections": {"default": settings.PSQL_CONNECT_STR},
"apps": { "apps": {
"models": { "models": {
"models": modules.get("models", []) + ["aerich.models"], "models": modules.get("models", []) + ["aerich.models"],
@@ -23,7 +24,7 @@ TORTOISE_ORM = {
async def init_db(): async def init_db():
await Tortoise.init(db_url=db_url, modules=modules) await Tortoise.init(db_url=settings.PSQL_CONNECT_STR, modules=modules)
async def migrate_db(): async def migrate_db():
+14 -3
View File
@@ -1,10 +1,14 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
from tortoise import Tortoise
from config import settings from config import settings
from modules.assets.router import router as asset_router 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 tortoise.contrib.fastapi import register_tortoise from tortoise.contrib.fastapi import register_tortoise
from database import db_url, modules from database import modules
app = FastAPI( app = FastAPI(
title=settings.PROJECT_NAME, title=settings.PROJECT_NAME,
@@ -12,20 +16,27 @@ app = FastAPI(
summary=settings.PROJECT_SUMMARY, summary=settings.PROJECT_SUMMARY,
) )
Tortoise.init_models(modules, "models")
register_tortoise( register_tortoise(
app, app,
db_url=db_url, db_url=settings.PSQL_CONNECT_STR,
modules=modules, modules=modules,
generate_schemas=True, generate_schemas=True,
add_exception_handlers=True, add_exception_handlers=True,
) )
app.include_router(auth_router)
app.include_router(users_router)
app.include_router(organizations_router)
app.include_router(asset_router) 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") @app.get("/ping")
async def ping() -> JSONResponse: async def ping() -> JSONResponse:
return JSONResponse("PONG") return JSONResponse("PONG")
@@ -0,0 +1,85 @@
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "asset" (
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(128) NOT NULL
);
CREATE TABLE IF NOT EXISTS "acl" (
"id" UUID NOT NULL PRIMARY KEY,
"READ" BOOL NOT NULL DEFAULT False,
"WRITE" BOOL NOT NULL DEFAULT False,
"REPORT" BOOL NOT NULL DEFAULT False,
"MANAGE" BOOL NOT NULL DEFAULT False,
"ADMIN" BOOL NOT NULL DEFAULT False
);
COMMENT ON TABLE "acl" IS 'ACL';
CREATE TABLE IF NOT EXISTS "organization" (
"created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"disabled_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(128) NOT NULL,
"type" VARCHAR(128) NOT NULL,
"disabled" BOOL NOT NULL DEFAULT False
);
COMMENT ON TABLE "organization" IS 'Organization';
CREATE TABLE IF NOT EXISTS "user" (
"created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"disabled_at" TIMESTAMPTZ,
"id" UUID 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" BOOL NOT NULL DEFAULT False
);
COMMENT ON TABLE "user" IS 'User';
CREATE TABLE IF NOT EXISTS "token" (
"created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"disabled_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"token_type" VARCHAR(128) NOT NULL DEFAULT 'Bearer',
"access_token" VARCHAR(128),
"refresh_token" VARCHAR(128),
"disabled" BOOL NOT NULL DEFAULT False,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
COMMENT ON TABLE "token" IS 'Token';
CREATE TABLE IF NOT EXISTS "membership" (
"created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
"disabled_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"disabled" BOOL NOT NULL DEFAULT False,
"acl_id" UUID NOT NULL REFERENCES "acl" ("id") ON DELETE CASCADE,
"organization_id" UUID NOT NULL REFERENCES "organization" ("id") ON DELETE CASCADE,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
COMMENT ON TABLE "membership" IS 'Membership';
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS "Membership" (
"organization_id" UUID NOT NULL REFERENCES "organization" ("id") ON DELETE NO ACTION,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE NO ACTION
);
CREATE UNIQUE INDEX IF NOT EXISTS "uidx_Membership_organiz_b0a446" ON "Membership" ("organization_id", "user_id");
CREATE TABLE IF NOT EXISTS "Membership" (
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE NO ACTION,
"organization_id" UUID NOT NULL REFERENCES "organization" ("id") ON DELETE NO ACTION
);
CREATE UNIQUE INDEX IF NOT EXISTS "uidx_Membership_user_id_cc48d3" ON "Membership" ("user_id", "organization_id");"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
@@ -2,7 +2,7 @@ from uuid import UUID
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from .models import Asset from modules.assets.models import Asset
router = APIRouter( router = APIRouter(
prefix="/assets" prefix="/assets"
@@ -10,17 +10,16 @@ router = APIRouter(
@router.get("/") @router.get("/")
async def get_all_assets(): async def get_all_assets():
return await Asset.get_or_none() pass
@router.post("/") @router.post("/")
async def create_asset(name: str): async def create_asset(name: str):
asset = await Asset.create(name=name) pass
return asset
@router.delete("/", status_code=204) @router.delete("/", status_code=204)
async def delete_asset(remove_id: UUID): async def delete_asset(remove_id: UUID):
await Asset.filter(id=remove_id).delete() pass
@router.get("/{asset_id}") @router.get("/{asset_id}")
async def get_asset(asset_id: UUID): async def get_asset(asset_id: UUID):
return Asset.filter(id=asset_id).get_or_none() pass
+2 -2
View File
@@ -15,8 +15,8 @@ class Token(Model, CMDMixin):
""" """
id: uuid = fields.UUIDField(primary_key=True) id: uuid = fields.UUIDField(primary_key=True)
user: uuid = fields.ForeignKeyField("modules.users.models.User") user: uuid = fields.ForeignKeyField("models.User")
token_type: str = fields.CharField(max_length=128, default="bearer") token_type: str = fields.CharField(max_length=128, default="Bearer")
access_token: str = fields.CharField(max_length=128, null=True) access_token: str = fields.CharField(max_length=128, null=True)
refresh_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) disabled: bool = fields.BooleanField(default=False)
+22 -5
View File
@@ -1,27 +1,32 @@
from datetime import timedelta
from typing import Annotated from typing import Annotated
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from utils import create_token, crypt_password
from models import Token
from modules.users.models import User from modules.users.models import User
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from config import settings from config import settings
from tortoise.expressions import Q from tortoise.expressions import Q
from authlib.jose import jwt # type: ignore from schemas import TokenModel
router = APIRouter(prefix="/auth") router = APIRouter(prefix="/auth")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
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 crypt = settings.CRYPT
@router.post("/") @router.post("/", response_model=TokenModel)
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]): async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
user: User = await User.filter(Q(email=form.username)).get_or_none() user: User = await User.filter(
Q(email=form.username) & Q(password=crypt_password(form.password))
).get_or_none()
if user is None: if user is None:
HTTPException(status_code=401, detail=error) HTTPException(status_code=401, detail=error)
@@ -29,6 +34,18 @@ async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
if user.check_against_password(form.password) is False: if user.check_against_password(form.password) is False:
HTTPException(status_code=401, detail=error) HTTPException(status_code=401, detail=error)
return JSONResponse(
await Token.create(
user=user.id,
access_token=create_token(
user_id=user.id, offset=timedelta(settings.ACCESS_TOKEN_EXPIRE_MIN)
),
refresh_token=create_token(
user_id=user.id, offset=timedelta(settings.REFRESH_TOKEN_EXPIRE_MIN)
),
)
)
@router.post("/refresh") @router.post("/refresh")
async def refresh_login(): async def refresh_login():
@@ -1,7 +1,5 @@
from tortoise.contrib.pydantic import pydantic_model_creator from tortoise.contrib.pydantic import pydantic_model_creator
from src.models import Organization, User from modules.auth.models import Token
OrganizationModel = pydantic_model_creator(Organization) TokenModel = pydantic_model_creator(Token)
UserModel = pydantic_model_creator(User)
@@ -0,0 +1,36 @@
import uuid, time
from config import settings
from joserfc import jwt # type: ignore
from joserfc.jwt import OctKey # type: ignore
crypt = settings.CRYPT
def crypt_password(password) -> str:
"""
Creates a hash from the "Password" given, that can then be checked against the DB.
"""
return crypt.hash(password, settings.HASHING_SCHEME)
def create_token(user_id: uuid, offset: float) -> str:
"""
Creates a JWT token
"""
curr_time = int(time.time())
return jwt.encode(
{"alg": settings.HASHING_SCHEME, "typ": "JWT"},
{
"iss": "",
"sub": user_id,
"nbf": curr_time,
"iat": curr_time,
"exp": int(curr_time + offset),
},
OctKey.import_key(settings.SECRET_KEY),
)
def decode_token(token: str):
pass
@@ -0,0 +1,17 @@
from fastapi import APIRouter
router = APIRouter(prefix="/organizations")
@router.get("/")
def all_organizations():
pass
@router.delete("/")
def delete_organization():
pass
@router.post("/create")
def create_organization():
pass
@@ -0,0 +1,6 @@
from tortoise.contrib.pydantic import pydantic_model_creator
from modules.organizations.models import Organization
OrganizationModel = pydantic_model_creator(Organization)
@@ -33,7 +33,7 @@ class User(Model, CMDMixin):
on_delete=fields.NO_ACTION, on_delete=fields.NO_ACTION,
) )
disabled: bool = fields.BooleanField(default=False) disabled: bool = fields.BooleanField(default=False)
tokens = fields.ForeignKeyField("modules.auth.models.Token") # tokens = fields.ForeignKeyField("models.Token")
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.id} - {self.name} {self.surname}" return f"{self.id} - {self.name} {self.surname}"
@@ -0,0 +1,12 @@
from fastapi import APIRouter
router = APIRouter(prefix="/users")
@router.get("/")
def get_all_users():
pass
@router.get("/me")
def get_user():
pass
@@ -0,0 +1,5 @@
from tortoise.contrib.pydantic import pydantic_model_creator
from modules.users.models import User
UserModel = pydantic_model_creator(User)
@@ -4,6 +4,6 @@ python-dotenv>=0.21.0
tortoise-orm[asyncpg]>=0.22.1 tortoise-orm[asyncpg]>=0.22.1
uvicorn>=0.31.1 uvicorn>=0.31.1
black>=24.10.0 black>=24.10.0
authlib>=1.3.2 joserfc>=1.0.1
passlib>=1.7.4 passlib>=1.7.4
pytz>=2024.2 pytz>=2024.2