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 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()
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user