Setup database connection, fix docker-compose, setup aerich migrations and more
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
|
||||
from passlib.context import CryptContext # type: ignore
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore
|
||||
from passlib.context import CryptContext # type: ignore
|
||||
import pytz
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "Open Asset Manager"
|
||||
PROJECT_NAME: str = "StoneEdge Asset Management System"
|
||||
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
|
||||
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
|
||||
REFRESH_TOKEN_EXPIRE_MIN: int = 60
|
||||
DEFAULT_TIMEZONE: str = pytz.UTC._tzname
|
||||
@@ -17,4 +17,5 @@ class Settings(BaseSettings):
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -2,17 +2,18 @@ from typing_extensions import Any
|
||||
from tortoise import Tortoise
|
||||
from config import settings
|
||||
|
||||
db_url = settings.PSQL_CONNECT_STR
|
||||
modules: dict[str, Any] = {
|
||||
"models": [
|
||||
".models",
|
||||
".modules.auth.models",
|
||||
".modules.assets.models",
|
||||
"models",
|
||||
"modules.assets.models",
|
||||
"modules.auth.models",
|
||||
"modules.users.models",
|
||||
"modules.organizations.models",
|
||||
]
|
||||
}
|
||||
|
||||
TORTOISE_ORM = {
|
||||
"connections": {"default": db_url},
|
||||
"connections": {"default": settings.PSQL_CONNECT_STR},
|
||||
"apps": {
|
||||
"models": {
|
||||
"models": modules.get("models", []) + ["aerich.models"],
|
||||
@@ -23,7 +24,7 @@ TORTOISE_ORM = {
|
||||
|
||||
|
||||
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():
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.responses import RedirectResponse
|
||||
from tortoise import Tortoise
|
||||
from config import settings
|
||||
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 database import db_url, modules
|
||||
from database import modules
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
@@ -12,20 +16,27 @@ app = FastAPI(
|
||||
summary=settings.PROJECT_SUMMARY,
|
||||
)
|
||||
|
||||
Tortoise.init_models(modules, "models")
|
||||
|
||||
register_tortoise(
|
||||
app,
|
||||
db_url=db_url,
|
||||
db_url=settings.PSQL_CONNECT_STR,
|
||||
modules=modules,
|
||||
generate_schemas=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.get("/")
|
||||
async def main():
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
|
||||
@app.get("/ping")
|
||||
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 .models import Asset
|
||||
from modules.assets.models import Asset
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/assets"
|
||||
@@ -10,17 +10,16 @@ router = APIRouter(
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_assets():
|
||||
return await Asset.get_or_none()
|
||||
pass
|
||||
|
||||
@router.post("/")
|
||||
async def create_asset(name: str):
|
||||
asset = await Asset.create(name=name)
|
||||
return asset
|
||||
pass
|
||||
|
||||
@router.delete("/", status_code=204)
|
||||
async def delete_asset(remove_id: UUID):
|
||||
await Asset.filter(id=remove_id).delete()
|
||||
pass
|
||||
|
||||
@router.get("/{asset_id}")
|
||||
async def get_asset(asset_id: UUID):
|
||||
return Asset.filter(id=asset_id).get_or_none()
|
||||
pass
|
||||
|
||||
@@ -15,8 +15,8 @@ class Token(Model, CMDMixin):
|
||||
"""
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
from datetime import timedelta
|
||||
from typing import Annotated
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
|
||||
from fastapi.routing import APIRouter
|
||||
from utils import create_token, crypt_password
|
||||
from models import Token
|
||||
from modules.users.models import User
|
||||
from fastapi import Depends, HTTPException
|
||||
from config import settings
|
||||
from tortoise.expressions import Q
|
||||
from authlib.jose import jwt # type: ignore
|
||||
from schemas import TokenModel
|
||||
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = settings.ACCESS_TOKEN_EXPIRE_MIN
|
||||
|
||||
error: str = "E-Mail Address or password is incorrect"
|
||||
|
||||
crypt = settings.CRYPT
|
||||
|
||||
|
||||
@router.post("/")
|
||||
@router.post("/", response_model=TokenModel)
|
||||
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:
|
||||
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:
|
||||
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")
|
||||
async def refresh_login():
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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)
|
||||
|
||||
UserModel = pydantic_model_creator(User)
|
||||
TokenModel = pydantic_model_creator(Token)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
disabled: bool = fields.BooleanField(default=False)
|
||||
tokens = fields.ForeignKeyField("modules.auth.models.Token")
|
||||
# tokens = fields.ForeignKeyField("models.Token")
|
||||
|
||||
def __str__(self) -> str:
|
||||
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
|
||||
uvicorn>=0.31.1
|
||||
black>=24.10.0
|
||||
authlib>=1.3.2
|
||||
joserfc>=1.0.1
|
||||
passlib>=1.7.4
|
||||
pytz>=2024.2
|
||||
|
||||
Reference in New Issue
Block a user