diff --git a/api/asset_manager/src/config.py b/api/asset_manager/src/config.py index 6674fae1..1dfd3aaa 100644 --- a/api/asset_manager/src/config.py +++ b/api/asset_manager/src/config.py @@ -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() diff --git a/api/asset_manager/src/database.py b/api/asset_manager/src/database.py index a72f712d..4c24af70 100644 --- a/api/asset_manager/src/database.py +++ b/api/asset_manager/src/database.py @@ -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(): diff --git a/api/asset_manager/src/main.py b/api/asset_manager/src/main.py index 41229ed8..13b317f6 100644 --- a/api/asset_manager/src/main.py +++ b/api/asset_manager/src/main.py @@ -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") \ No newline at end of file + return JSONResponse("PONG") diff --git a/api/asset_manager/src/migrations/models/0_20250122175143_init.py b/api/asset_manager/src/migrations/models/0_20250122175143_init.py new file mode 100644 index 00000000..65f2309c --- /dev/null +++ b/api/asset_manager/src/migrations/models/0_20250122175143_init.py @@ -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 """ + """ diff --git a/api/asset_manager/src/modules/assets/router.py b/api/asset_manager/src/modules/assets/router.py index fbe3f3f6..ded77ff1 100644 --- a/api/asset_manager/src/modules/assets/router.py +++ b/api/asset_manager/src/modules/assets/router.py @@ -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 diff --git a/api/asset_manager/src/modules/auth/models.py b/api/asset_manager/src/modules/auth/models.py index a0649f77..31e50236 100644 --- a/api/asset_manager/src/modules/auth/models.py +++ b/api/asset_manager/src/modules/auth/models.py @@ -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) diff --git a/api/asset_manager/src/modules/auth/router.py b/api/asset_manager/src/modules/auth/router.py index 63c6a3de..a90fab68 100644 --- a/api/asset_manager/src/modules/auth/router.py +++ b/api/asset_manager/src/modules/auth/router.py @@ -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(): diff --git a/api/asset_manager/src/modules/auth/schemas.py b/api/asset_manager/src/modules/auth/schemas.py index 07b14709..e884e311 100644 --- a/api/asset_manager/src/modules/auth/schemas.py +++ b/api/asset_manager/src/modules/auth/schemas.py @@ -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) diff --git a/api/asset_manager/src/modules/auth/utils.py b/api/asset_manager/src/modules/auth/utils.py new file mode 100644 index 00000000..cb9b59c3 --- /dev/null +++ b/api/asset_manager/src/modules/auth/utils.py @@ -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 diff --git a/api/asset_manager/src/migrations/models/__init__.py b/api/asset_manager/src/modules/organizations/__init__.py similarity index 100% rename from api/asset_manager/src/migrations/models/__init__.py rename to api/asset_manager/src/modules/organizations/__init__.py diff --git a/api/asset_manager/src/modules/organizations/router.py b/api/asset_manager/src/modules/organizations/router.py new file mode 100644 index 00000000..b3467bae --- /dev/null +++ b/api/asset_manager/src/modules/organizations/router.py @@ -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 + diff --git a/api/asset_manager/src/modules/organizations/schemas.py b/api/asset_manager/src/modules/organizations/schemas.py new file mode 100644 index 00000000..272a700d --- /dev/null +++ b/api/asset_manager/src/modules/organizations/schemas.py @@ -0,0 +1,6 @@ +from tortoise.contrib.pydantic import pydantic_model_creator + +from modules.organizations.models import Organization + +OrganizationModel = pydantic_model_creator(Organization) + diff --git a/api/asset_manager/src/modules/users/__init__.py b/api/asset_manager/src/modules/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/asset_manager/src/modules/users/models.py b/api/asset_manager/src/modules/users/models.py index 9d6ad42d..e16a69c8 100644 --- a/api/asset_manager/src/modules/users/models.py +++ b/api/asset_manager/src/modules/users/models.py @@ -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}" diff --git a/api/asset_manager/src/modules/users/router.py b/api/asset_manager/src/modules/users/router.py index e69de29b..85de7f37 100644 --- a/api/asset_manager/src/modules/users/router.py +++ b/api/asset_manager/src/modules/users/router.py @@ -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 \ No newline at end of file diff --git a/api/asset_manager/src/modules/users/schemas.py b/api/asset_manager/src/modules/users/schemas.py index e69de29b..6291c05b 100644 --- a/api/asset_manager/src/modules/users/schemas.py +++ b/api/asset_manager/src/modules/users/schemas.py @@ -0,0 +1,5 @@ +from tortoise.contrib.pydantic import pydantic_model_creator + +from modules.users.models import User + +UserModel = pydantic_model_creator(User) diff --git a/api/asset_manager/src/requirements/requirements.txt b/api/asset_manager/src/requirements/requirements.txt index ecb692e2..a299c906 100644 --- a/api/asset_manager/src/requirements/requirements.txt +++ b/api/asset_manager/src/requirements/requirements.txt @@ -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