From 3e15aa772296ba3fdd0c9b5f0223e5f127b63f5a Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Fri, 10 Oct 2025 15:48:40 +0200 Subject: [PATCH 01/34] =?UTF-8?q?=F0=9F=90=9B=20Support=20apps=20without?= =?UTF-8?q?=20any=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/core/runtime.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ohmyapi/core/runtime.py b/src/ohmyapi/core/runtime.py index 576e275..e3d700e 100644 --- a/src/ohmyapi/core/runtime.py +++ b/src/ohmyapi/core/runtime.py @@ -154,7 +154,10 @@ class Project: tortoise_cfg = self.build_tortoise_config(db_url=db_url) # Prevent leaking other app's models to Aerich. - tortoise_cfg["apps"] = {app_label: tortoise_cfg["apps"][app_label]} + if app_label in tortoise_cfg["apps"].keys(): + tortoise_cfg["apps"] = {app_label: tortoise_cfg["apps"][app_label]} + else: + tortoise_cfg["apps"] = {app_label: {"default_connection": "default", "models": []}} # Append aerich.models to the models list of the target app only tortoise_cfg["apps"][app_label]["models"].append("aerich.models") @@ -403,7 +406,7 @@ class App: """ return { "models": [ - f"{self.name}.{m.__name__}" for m in self.models[f"{self.name}.models"] + f"{self.name}.{m.__name__}" for m in self._models[f"{self.name}.models"] ], "middlewares": self.__serialize_middleware(), "routes": self.__serialize_router(), From 2239480dc03d70d627fa10462d6a7b8581e1ce73 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Fri, 10 Oct 2025 15:49:14 +0200 Subject: [PATCH 02/34] =?UTF-8?q?=F0=9F=8E=A8=20Also=20export=20tortoise.q?= =?UTF-8?q?uery=5Futils.Prefetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/db/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ohmyapi/db/__init__.py b/src/ohmyapi/db/__init__.py index e13bdeb..0a634a9 100644 --- a/src/ohmyapi/db/__init__.py +++ b/src/ohmyapi/db/__init__.py @@ -1,5 +1,6 @@ from tortoise.expressions import Q from tortoise.manager import Manager +from tortoise.query_utils import Prefetch from tortoise.queryset import QuerySet from tortoise.signals import ( post_delete, From 1c42b44d410abf5b95ddf6c50a6b5d061ccfa6fb Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Fri, 10 Oct 2025 15:49:57 +0200 Subject: [PATCH 03/34] =?UTF-8?q?=F0=9F=94=96=200.4.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01e64e6..038c3f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.4.3" +version = "0.4.4" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index f929707..993fbd9 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.4.3" +__VERSION__ = "0.4.4" From 643a6b2eb7ec1f374ffac057165ac3a5e70c9682 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 01:07:55 +0200 Subject: [PATCH 04/34] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Add=20optionally?= =?UTF-8?q?=5Fauthenticated=20permission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/routes.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 8a81389..2f1cb2a 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -1,6 +1,6 @@ import time from enum import Enum -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import jwt import settings @@ -124,6 +124,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: return user +async def optionally_authenticated(token: Optional[str] = Depends(oauth2_scheme)) -> Optional[User]: + if token is None: + return None + return await get_current_user(token) + + async def require_authenticated(current_user: User = Depends(get_current_user)) -> User: """Ensure the current user is an admin.""" if not current_user: From d7f7db338f6a50ea9908c4bd0d91f0c9bd750042 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 01:08:50 +0200 Subject: [PATCH 05/34] =?UTF-8?q?=F0=9F=94=96=200.4.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 038c3f6..9a7592c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.4.4" +version = "0.4.5" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 993fbd9..31f0221 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.4.4" +__VERSION__ = "0.4.5" From cf106e88557940bb719d4a20a420e63662231e80 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 01:20:32 +0200 Subject: [PATCH 06/34] =?UTF-8?q?=F0=9F=90=9B=20maybe=5Fauthenticated=20wi?= =?UTF-8?q?th=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/permissions.py | 1 + src/ohmyapi/builtin/auth/routes.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ohmyapi/builtin/auth/permissions.py b/src/ohmyapi/builtin/auth/permissions.py index 578dbd3..e7be37e 100644 --- a/src/ohmyapi/builtin/auth/permissions.py +++ b/src/ohmyapi/builtin/auth/permissions.py @@ -1,6 +1,7 @@ from .routes import ( get_current_user, get_token, + maybe_authenticated, require_admin, require_authenticated, require_group, diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 2f1cb2a..3b37f59 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -124,7 +124,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: return user -async def optionally_authenticated(token: Optional[str] = Depends(oauth2_scheme)) -> Optional[User]: +async def maybe_authenticated(token: Optional[str] = Depends(oauth2_scheme)) -> Optional[User]: if token is None: return None return await get_current_user(token) From 66176e9af7decf39dd38d6b33f259c5274cf3349 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 01:21:03 +0200 Subject: [PATCH 07/34] =?UTF-8?q?=F0=9F=94=96=200.4.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a7592c..a5925a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.4.5" +version = "0.4.6" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 31f0221..8824968 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.4.5" +__VERSION__ = "0.4.6" From 80163ce994487b7a5471ffa38261960e72e8fd51 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 02:06:42 +0200 Subject: [PATCH 08/34] =?UTF-8?q?=F0=9F=90=9B=20Make=20settings=20module?= =?UTF-8?q?=20available,=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/core/runtime.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ohmyapi/core/runtime.py b/src/ohmyapi/core/runtime.py index e3d700e..8fedc3c 100644 --- a/src/ohmyapi/core/runtime.py +++ b/src/ohmyapi/core/runtime.py @@ -40,6 +40,14 @@ class Project: if str(self.project_path) not in sys.path: sys.path.insert(0, str(self.project_path)) + # Load settings.py + try: + self.settings = importlib.import_module("settings") + except Exception as e: + raise RuntimeError( + f"Failed to import project settings from {self.project_path}" + ) from e + # Alias builtin apps as ohmyapi_. # We need this, because Tortoise app-names may not include dots `.`. spec = importlib.util.find_spec("ohmyapi.builtin") @@ -57,14 +65,6 @@ class Project: except ModuleNotFoundError: pass - # Load settings.py - try: - self.settings = importlib.import_module("settings") - except Exception as e: - raise RuntimeError( - f"Failed to import project settings from {self.project_path}" - ) from e - # Load installed apps for app_name in getattr(self.settings, "INSTALLED_APPS", []): self._apps[app_name] = App(self, name=app_name) From de043ddd978ad0a38dc0e4bfe5acfaf8de4f0465 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 02:19:53 +0200 Subject: [PATCH 09/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20authenticate=20metho?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/models.py | 2 +- src/ohmyapi/builtin/auth/routes.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index bdb4451..831ebae 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -66,7 +66,7 @@ class User(Model): return pwd_context.verify(raw_password, self.password_hash) @classmethod - async def authenticate_username(cls, username: str, password: str) -> Optional["User"]: + async def authenticate(cls, username: str, password: str) -> Optional["User"]: """Authenticate a user by username and password.""" user = await cls.filter(username=username).first() if user and user.verify_password(password): diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 3b37f59..04169c1 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -28,7 +28,6 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") class ClaimsUser(BaseModel): username: str - email: str is_admin: bool is_staff: bool @@ -70,7 +69,6 @@ def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claim sub=str(user.id), user=ClaimsUser( username=user.username, - email=user.email, is_admin=user.is_admin, is_staff=user.is_staff, ), From a45f03b92f82618f34918a3373c8f6f61189bd98 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 02:20:59 +0200 Subject: [PATCH 10/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20maybe=5Fauthenticate?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/routes.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 04169c1..3d7be7f 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -2,14 +2,16 @@ import time from enum import Enum from typing import Any, Dict, List, Optional -import jwt -import settings -from fastapi import APIRouter, Body, Depends, Header, HTTPException, status -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi import APIRouter, Body, Depends, Header, HTTPException, Request, status +from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm +from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from ohmyapi.builtin.auth.models import Group, User +import jwt +import settings + # Router router = APIRouter(prefix="/auth", tags=["Auth"]) @@ -23,7 +25,21 @@ REFRESH_TOKEN_EXPIRE_SECONDS = getattr( settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60 ) +class OptionalOAuth2PasswordBearer(OAuth2): + def __init__(self, tokenUrl: str): + super().__init__(flows={"password": {"tokenUrl": tokenUrl}}, scheme_name="OAuth2PasswordBearer") + + async def __call__(self, request: Request) -> Optional[str]: + authorization: str = request.headers.get("Authorization") + scheme, param = get_authorization_scheme_param(authorization) + if not authorization or scheme.lower() != "bearer": + # No token provided — just return None + return None + return param + + oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") +oauth2_optional_scheme = OptionalOAuth2PasswordBearer(tokenUrl="/auth/login") class ClaimsUser(BaseModel): @@ -122,7 +138,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: return user -async def maybe_authenticated(token: Optional[str] = Depends(oauth2_scheme)) -> Optional[User]: +async def maybe_authenticated(token: Optional[str] = Depends(oauth2_optional_scheme)) -> Optional[User]: if token is None: return None return await get_current_user(token) From 74f625ab1df19c20515540aa495721f0a1d7e834 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 02:22:15 +0200 Subject: [PATCH 11/34] =?UTF-8?q?=F0=9F=94=96=200.4.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a5925a5..16265c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.4.6" +version = "0.4.7" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 8824968..01c4965 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.4.6" +__VERSION__ = "0.4.7" From acd4844a257b897aeb2f2f0e1e9a28c865c2a9ac Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 04:31:30 +0200 Subject: [PATCH 12/34] =?UTF-8?q?=F0=9F=8E=A8=20Export=20tortoise.function?= =?UTF-8?q?s=20as=20ohmyapi.db.functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/db/functions.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/ohmyapi/db/functions.py diff --git a/src/ohmyapi/db/functions.py b/src/ohmyapi/db/functions.py new file mode 100644 index 0000000..c5cf5de --- /dev/null +++ b/src/ohmyapi/db/functions.py @@ -0,0 +1 @@ +from tortoise.functions import * From ce47e3f60e915ceb7acc3235be122aeee473a247 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 13:03:31 +0200 Subject: [PATCH 13/34] =?UTF-8?q?=F0=9F=8E=A8=20Auto-prefix=20table=5Fname?= =?UTF-8?q?s=20with=20"{app=5Flabel}=5F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no support for auto-prefixing implicit proxy-tables in Tortoise. If you need to prefix a proxy-table, explicitly define the Model for the proxy table. Being an explicit model, it will then be auto-prefixed. --- src/ohmyapi/db/model/model.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/ohmyapi/db/model/model.py b/src/ohmyapi/db/model/model.py index 929ff60..7ab918f 100644 --- a/src/ohmyapi/db/model/model.py +++ b/src/ohmyapi/db/model/model.py @@ -31,6 +31,18 @@ UUID.__get_pydantic_core_schema__ = classmethod(__uuid_schema_monkey_patch) class ModelMeta(type(TortoiseModel)): def __new__(mcls, name, bases, attrs): + meta = attrs.get("Meta", None) + if meta is None: + class Meta: + pass + meta = Meta + attrs["Meta"] = meta + + if not hasattr(meta, "table"): + # Use first part of module as app_label + app_label = attrs.get("__module__", "").replace("ohmyapi.builtin.", "ohmyapi_").split(".")[0] + setattr(meta, "table", f"{app_label}_{name.lower()}") + # Grab the Schema class for further processing. schema_opts = attrs.get("Schema", None) From 2e1ec5d780fb875cb3000df219f27f50e928df5d Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 13:06:34 +0200 Subject: [PATCH 14/34] =?UTF-8?q?=F0=9F=94=96=200.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16265c6..e3b4ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.4.7" +version = "0.5.0" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 01c4965..dc77141 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.4.7" +__VERSION__ = "0.5.0" From d494396728f5fb11a86f3431575af05fbf840582 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 13:12:48 +0200 Subject: [PATCH 15/34] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/auth.md b/docs/auth.md index 2526966..d26c1fe 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -40,6 +40,6 @@ router = APIRouter() @router.get("/") -def get(user: auth.User = Depends(permissions.required_authenticated)): +def get(user: auth.User = Depends(permissions.require_authenticated)): ... ``` From e25c9d1715d7075327be7df8e977a87aa8a9e856 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 13:30:46 +0200 Subject: [PATCH 16/34] =?UTF-8?q?=F0=9F=A9=B9=20Define=20explicit=20user?= =?UTF-8?q?=5Fgroups=20proxy-table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/models.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index 831ebae..aeaa4ae 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -39,7 +39,7 @@ class User(Model): is_admin: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False) groups: field.ManyToManyRelation[Group] = field.ManyToManyField( - "ohmyapi_auth.Group", related_name="users", through="usergroups" + "ohmyapi_auth.Group", related_name="users", through="ohmyapi_auth_user_groups" ) class Schema: @@ -72,3 +72,20 @@ class User(Model): if user and user.verify_password(password): return user return None + + +class UserGroups(Model): + user: field.ForeignKeyRelation[User] = field.ForeignKeyField( + "ohmyapi_auth.User", + related_name="user_groups", + index=True, + ) + group: field.ForeignKeyRelation[Group] = field.ForeignKeyField( + "ohmyapi_auth.Group", + related_name="group_users", + index=True, + ) + + class Meta: + table = "ohmyapi_auth_user_groups" + constraints = [("UNIQUE", ("user_id", "group_id"))] From 812e89ede931eaf6c4a60ecaf3d0559f675b8076 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 11 Oct 2025 13:31:28 +0200 Subject: [PATCH 17/34] =?UTF-8?q?=F0=9F=94=96=200.5.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 +++- src/ohmyapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3b4ba7..b33d472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.0" +version = "0.5.1" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] @@ -30,6 +30,8 @@ ipython = ">=9.5.0,<10.0.0" black = "^25.9.0" isort = "^6.0.1" mkdocs = "^1.6.1" +pytest = "^8.4.2" +httpx = "^0.28.1" [project.optional-dependencies] auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index dc77141..ceaf150 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.0" +__VERSION__ = "0.5.1" From 6b87bfeefbd4a829c71ba4a170ad2f75fc116cca Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Wed, 22 Oct 2025 11:25:40 +0200 Subject: [PATCH 18/34] =?UTF-8?q?=F0=9F=90=9B=20email=5Fhash=20is=20not=20?= =?UTF-8?q?optional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index aeaa4ae..d4c2f12 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -34,7 +34,7 @@ class Group(Model): class User(Model): id: UUID = field.data.UUIDField(pk=True) username: str = field.CharField(max_length=150, unique=True) - email_hash: Optional[str] = field.CharField(max_length=255, unique=True, index=True) + email_hash: str = field.CharField(max_length=255, unique=True, index=True) password_hash: str = field.CharField(max_length=128) is_admin: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False) From 58f1387aafae2f8046c58d3303d4588c9c23a729 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Wed, 22 Oct 2025 11:26:50 +0200 Subject: [PATCH 19/34] =?UTF-8?q?=F0=9F=94=96=200.5.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b33d472..9a1ece0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.1" +version = "0.5.2" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index ceaf150..483d205 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.1" +__VERSION__ = "0.5.2" From 10681cc15b9c415e922214a3c87002678ce199ed Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Wed, 22 Oct 2025 11:55:47 +0200 Subject: [PATCH 20/34] =?UTF-8?q?=F0=9F=9A=9A=20Move=20hmac=5Fhash()=20to?= =?UTF-8?q?=20ohmyapi=5Fauth.utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/models.py | 11 +---------- src/ohmyapi/builtin/auth/utils.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/ohmyapi/builtin/auth/utils.py diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index d4c2f12..e8894c1 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -1,6 +1,3 @@ -import hmac -import hashlib -import base64 from functools import wraps from secrets import token_bytes from typing import List, Optional @@ -12,15 +9,9 @@ from tortoise.contrib.pydantic import pydantic_queryset_creator from ohmyapi.db import Model, field, Q from ohmyapi.router import HTTPException -import settings +from .utils import hmac_hash pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") -SECRET_KEY = getattr(settings, "SECRET_KEY", "OhMyAPI Secret Key") - - -def hmac_hash(data: str) -> str: - digest = hmac.new(SECRET_KEY.encode("UTF-8"), data.encode("utf-8"), hashlib.sha256).digest() - return base64.urlsafe_b64encode(digest).decode("utf-8") class Group(Model): diff --git a/src/ohmyapi/builtin/auth/utils.py b/src/ohmyapi/builtin/auth/utils.py new file mode 100644 index 0000000..e54a5da --- /dev/null +++ b/src/ohmyapi/builtin/auth/utils.py @@ -0,0 +1,17 @@ +import base64 +import hashlib +import hmac + +import settings + +SECRET_KEY = getattr(settings, "SECRET_KEY", "OhMyAPI Secret Key") + + +def hmac_hash(data: str) -> str: + digest = hmac.new( + SECRET_KEY.encode("UTF-8"), + data.encode("utf-8"), + hashlib.sha256, + ).digest() + return base64.urlsafe_b64encode(digest).decode("utf-8") + From b50cbc43417c832d47639d5f9f57b00e61401a20 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 25 Oct 2025 11:17:05 +0200 Subject: [PATCH 21/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20/auth/refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/routes.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 3d7be7f..efda1b2 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -200,10 +200,14 @@ async def login(form_data: LoginRequest = Body(...)): ) +class TokenRefresh(BaseModel): + refresh_token: str + + @router.post("/refresh", response_model=AccessToken) -async def refresh_token(refresh_token: str): +async def refresh_token(refresh_token: TokenRefresh = Body(...)): """Exchange refresh token for new access token.""" - payload = decode_token(refresh_token) + payload = decode_token(refresh_token.refresh_token) if payload.get("type") != "refresh": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" @@ -219,7 +223,7 @@ async def refresh_token(refresh_token: str): new_access = create_token( claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS ) - return AccessToken(token_type="bearer", access_token=access_token) + return AccessToken(token_type="bearer", access_token=new_access) @router.get("/introspect", response_model=Dict[str, Any]) From 4a5bafd889de962b180e984380a81ebb5c026478 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 25 Oct 2025 11:18:07 +0200 Subject: [PATCH 22/34] =?UTF-8?q?=F0=9F=94=96=200.5.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a1ece0..e4b283e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.2" +version = "0.5.3" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 483d205..e1de76b 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.2" +__VERSION__ = "0.5.3" From 715b7a030a0e0fc4c834a347a3a33629f65b2314 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sun, 26 Oct 2025 21:47:03 +0100 Subject: [PATCH 23/34] =?UTF-8?q?=F0=9F=90=9B=20Createsuperuser=20in=20sin?= =?UTF-8?q?gle=20asyncio=20task?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/cli.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ohmyapi/cli.py b/src/ohmyapi/cli.py index a530424..fd41084 100644 --- a/src/ohmyapi/cli.py +++ b/src/ohmyapi/cli.py @@ -165,6 +165,13 @@ def createsuperuser(root: str = "."): ) user.set_email(email) user.set_password(password1) - asyncio.run(project.init_orm()) - asyncio.run(user.save()) - asyncio.run(project.close_orm()) + + async def _run(): + await project.init_orm() + user = ohmyapi_auth.models.User(username=username, is_staff=True, is_admin=True) + user.set_email(email) + user.set_password(password1) + await user.save() + await project.close_orm() + + asyncio.run(_run()) From 31f4da773c9755e6af477cf922b32c351044e66c Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sun, 26 Oct 2025 21:48:48 +0100 Subject: [PATCH 24/34] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 113 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index c30b765..57e1e68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "aerich" @@ -62,7 +62,7 @@ version = "4.11.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, @@ -216,7 +216,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -543,19 +543,66 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -564,6 +611,18 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + [[package]] name = "ipython" version = "9.6.0" @@ -1024,6 +1083,22 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1260,6 +1335,28 @@ files = [ {file = "pypika_tortoise-0.6.2.tar.gz", hash = "sha256:f95ab59d9b6454db2e8daa0934728458350a1f3d56e81d9d1debc8eebeff26b3"}, ] +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1488,7 +1585,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1604,7 +1701,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version == \"3.11\""} +markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -1719,4 +1816,4 @@ auth = ["argon2-cffi", "crypto", "passlib", "pyjwt", "python-multipart"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "3d301460081dada359d425d69feefc63c1e5135aa64b6f000f554bfc1231febd" +content-hash = "cc1604995d3b73ee302e63731dd300ea17c4d95d0cfc6c386626dd9a9f60e8a7" From ed30291a4cedfaed622ea2a4852c10c939e949c8 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sun, 26 Oct 2025 21:49:13 +0100 Subject: [PATCH 25/34] =?UTF-8?q?=F0=9F=94=96=200.5.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4b283e..cfb6e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.3" +version = "0.5.4" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index e1de76b..f8393e5 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.3" +__VERSION__ = "0.5.4" From 9d2e284da3aef100dbc2b3b15f5271462ff3bed6 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 27 Oct 2025 10:45:19 +0100 Subject: [PATCH 26/34] =?UTF-8?q?=F0=9F=90=9B=20Strict=20proxy-table=20fie?= =?UTF-8?q?ld=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This worked in SQlite3, but threw when using PostgreSQL. --- src/ohmyapi/builtin/auth/models.py | 6 +++++- src/ohmyapi/builtin/auth/routes.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index e8894c1..2d58ce1 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -30,7 +30,11 @@ class User(Model): is_admin: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False) groups: field.ManyToManyRelation[Group] = field.ManyToManyField( - "ohmyapi_auth.Group", related_name="users", through="ohmyapi_auth_user_groups" + "ohmyapi_auth.Group", + related_name="users", + through="ohmyapi_auth.UserGroups", + forward_key="user_id", + backward_key="group_id", ) class Schema: diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index efda1b2..855126f 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -214,7 +214,7 @@ async def refresh_token(refresh_token: TokenRefresh = Body(...)): ) user_id = payload.get("sub") - user = await User.filter(id=user_id).first() + user = await User.get(id=user_id) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" From 8c2cf01f4037b98cecca4b50af8d5577677f0e0a Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 27 Oct 2025 10:47:05 +0100 Subject: [PATCH 27/34] =?UTF-8?q?=F0=9F=94=96=200.5.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cfb6e5d..8f03096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.4" +version = "0.5.5" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index f8393e5..7bebe27 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.4" +__VERSION__ = "0.5.5" From 22ca522615a12ee4888fc0b2df59c269b9393d26 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 27 Oct 2025 11:03:12 +0100 Subject: [PATCH 28/34] =?UTF-8?q?=F0=9F=90=9B=20Catch=20invalid=20user=20r?= =?UTF-8?q?efresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/builtin/auth/routes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 855126f..0104a4e 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Request, st from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel +from tortoise.exceptions import DoesNotExist from ohmyapi.builtin.auth.models import Group, User @@ -214,7 +215,11 @@ async def refresh_token(refresh_token: TokenRefresh = Body(...)): ) user_id = payload.get("sub") - user = await User.get(id=user_id) + try: + user = await User.get(id=user_id) + except DoesNotExist: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" From 458ffc6b2cc1624cd9dc72def14ccce2ed0fcd00 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 27 Oct 2025 11:13:02 +0100 Subject: [PATCH 29/34] =?UTF-8?q?=F0=9F=94=96=200.5.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f03096..094c428 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.5" +version = "0.5.6" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 7bebe27..dfda844 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.5" +__VERSION__ = "0.5.6" From 7163fe778e025e29c8fa8635b04f968af023f6d9 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Tue, 28 Oct 2025 14:37:45 +0100 Subject: [PATCH 30/34] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20ohmyapi?= =?UTF-8?q?=5Fauth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove Group and UserGroups (should be handled by dedicated app, if even) - enforce User.Schema() include-fields --- docs/apps.md | 2 +- src/ohmyapi/builtin/auth/models.py | 58 +++++++++--------------------- src/ohmyapi/builtin/auth/routes.py | 5 ++- 3 files changed, 19 insertions(+), 46 deletions(-) diff --git a/docs/apps.md b/docs/apps.md index fdcf5c4..dc70830 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -1,7 +1,7 @@ # Apps Apps are a way to group database models and API routes that contextually belong together. -For example, OhMyAPI comes bundled with an `auth` app that carries a `User` and `Group` model and provides API endpoints for JWT authentication. +For example, OhMyAPI comes bundled with an `auth` app that carries a `User` model and provides API endpoints for JWT authentication. Apps help organizing projects by isolating individual components (or "features") from one another. diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index 2d58ce1..b9785c7 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -1,27 +1,16 @@ -from functools import wraps -from secrets import token_bytes -from typing import List, Optional -from uuid import UUID - -from passlib.context import CryptContext -from tortoise.contrib.pydantic import pydantic_queryset_creator - from ohmyapi.db import Model, field, Q from ohmyapi.router import HTTPException from .utils import hmac_hash +from datetime import datetime +from passlib.context import CryptContext +from typing import Optional +from uuid import UUID + pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") -class Group(Model): - id: UUID = field.data.UUIDField(pk=True) - name: str = field.CharField(max_length=42, index=True) - - def __str__(self): - return self.name if self.name else "" - - class User(Model): id: UUID = field.data.UUIDField(pk=True) username: str = field.CharField(max_length=150, unique=True) @@ -29,20 +18,22 @@ class User(Model): password_hash: str = field.CharField(max_length=128) is_admin: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False) - groups: field.ManyToManyRelation[Group] = field.ManyToManyField( - "ohmyapi_auth.Group", - related_name="users", - through="ohmyapi_auth.UserGroups", - forward_key="user_id", - backward_key="group_id", - ) + created_at: datetime = field.DatetimeField(auto_now_add=True) + updated_at: datetime = field.DatetimeField(auto_now=True) class Schema: - exclude = ["password_hash", "email_hash"] + include = { + "id", + "username", + "is_admin", + "is_staff" + "created_at", + "updated_at", + } def __str__(self): fields = { - 'username': self.username if self.username else "-", + 'username': self.username, 'is_admin': 'y' if self.is_admin else 'n', 'is_staff': 'y' if self.is_staff else 'n', } @@ -67,20 +58,3 @@ class User(Model): if user and user.verify_password(password): return user return None - - -class UserGroups(Model): - user: field.ForeignKeyRelation[User] = field.ForeignKeyField( - "ohmyapi_auth.User", - related_name="user_groups", - index=True, - ) - group: field.ForeignKeyRelation[Group] = field.ForeignKeyField( - "ohmyapi_auth.Group", - related_name="group_users", - index=True, - ) - - class Meta: - table = "ohmyapi_auth_user_groups" - constraints = [("UNIQUE", ("user_id", "group_id"))] diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 0104a4e..3fa2651 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -8,7 +8,7 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from tortoise.exceptions import DoesNotExist -from ohmyapi.builtin.auth.models import Group, User +from ohmyapi.builtin.auth.models import User import jwt import settings @@ -80,7 +80,7 @@ class TokenType(str, Enum): refresh = "refresh" -def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims: +def claims(token_type: TokenType, user: User = []) -> Claims: return Claims( type=token_type, sub=str(user.id), @@ -89,7 +89,6 @@ def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claim is_admin=user.is_admin, is_staff=user.is_staff, ), - roles=[g.name for g in groups], exp="", ) From a9b88d87d6ccda84a8456e898aee49ef23cf8030 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Tue, 28 Oct 2025 14:40:29 +0100 Subject: [PATCH 31/34] =?UTF-8?q?=F0=9F=94=96=200.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 094c428..51aad8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.5.6" +version = "0.6.0" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index dfda844..73e56e4 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.5.6" +__VERSION__ = "0.6.0" From b588ebcf8a866c628bc348dfe82cfd60f5107611 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Tue, 28 Oct 2025 14:45:18 +0100 Subject: [PATCH 32/34] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Remove=20roles=20?= =?UTF-8?q?claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- src/ohmyapi/builtin/auth/routes.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51aad8b..7a7a95c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.6.0" +version = "0.6.1" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 73e56e4..d9520d7 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.6.0" +__VERSION__ = "0.6.1" diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index 3fa2651..fa2d4e7 100644 --- a/src/ohmyapi/builtin/auth/routes.py +++ b/src/ohmyapi/builtin/auth/routes.py @@ -53,7 +53,6 @@ class Claims(BaseModel): type: str sub: str user: ClaimsUser - roles: List[str] exp: str From 5f80a7a86f2de03397e6260477a6d93bcf4f7cae Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Wed, 5 Nov 2025 21:29:25 +0100 Subject: [PATCH 33/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20model-free=20apps=20?= =?UTF-8?q?and=20middleware=20installer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ohmyapi/core/runtime.py | 17 +++++++++++------ src/ohmyapi/middleware/cors.py | 10 +++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/ohmyapi/core/runtime.py b/src/ohmyapi/core/runtime.py index 8fedc3c..dea8d78 100644 --- a/src/ohmyapi/core/runtime.py +++ b/src/ohmyapi/core/runtime.py @@ -342,9 +342,9 @@ class App: except ModuleNotFoundError: return - getter = getattr(mod, "get", None) - if getter is not None: - for middleware in getter(): + installer = getattr(mod, "install", None) + if installer is not None: + for middleware in installer(): self._middlewares.append(middleware) def __serialize_route(self, route): @@ -404,10 +404,15 @@ class App: """ Convenience method for serializing the runtime data. """ + # An app may come without any models + models = [] + if f"{self.name}.models" in self._models: + models = [ + f"{self.name}.{m.__name__}" + for m in self._models[f"{self.name}.models"] + ] return { - "models": [ - f"{self.name}.{m.__name__}" for m in self._models[f"{self.name}.models"] - ], + "models": models, "middlewares": self.__serialize_middleware(), "routes": self.__serialize_router(), } diff --git a/src/ohmyapi/middleware/cors.py b/src/ohmyapi/middleware/cors.py index 49852cd..77d49b6 100644 --- a/src/ohmyapi/middleware/cors.py +++ b/src/ohmyapi/middleware/cors.py @@ -15,12 +15,12 @@ CORS_CONFIG: Dict[str, Any] = getattr(settings, "MIDDLEWARE_CORS", {}) if not isinstance(CORS_CONFIG, dict): raise ValueError("MIDDLEWARE_CORS must be of type dict") -middleware = [ - (CORSMiddleware, { +middleware = ( + CORSMiddleware, + { "allow_origins": CORS_CONFIG.get("ALLOW_ORIGINS", DEFAULT_ORIGINS), "allow_credentials": CORS_CONFIG.get("ALLOW_CREDENTIALS", DEFAULT_CREDENTIALS), "allow_methods": CORS_CONFIG.get("ALLOW_METHODS", DEFAULT_METHODS), "allow_headers": CORS_CONFIG.get("ALLOW_HEADERS", DEFAULT_HEADERS), - }), -] - + } +) From b5691f3133e57b268d0d1b9f1e0bd8c9652e72d9 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Wed, 5 Nov 2025 21:37:34 +0100 Subject: [PATCH 34/34] =?UTF-8?q?=F0=9F=94=96=200.6.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- src/ohmyapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7a7a95c..6db719c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ohmyapi" -version = "0.6.1" +version = "0.6.2" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" license = "MIT" keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index d9520d7..4191a45 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.6.1" +__VERSION__ = "0.6.2"