From 9d2e284da3aef100dbc2b3b15f5271462ff3bed6 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 27 Oct 2025 10:45:19 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=90=9B=20Strict=20proxy-table=20field?= =?UTF-8?q?=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 2/9] =?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 3/9] =?UTF-8?q?=F0=9F=90=9B=20Catch=20invalid=20user=20ref?= =?UTF-8?q?resh?= 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 4/9] =?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 5/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20ohmyapi=5Fa?= =?UTF-8?q?uth?= 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 6/9] =?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 7/9] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Remove=20roles=20cl?= =?UTF-8?q?aim?= 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 8/9] =?UTF-8?q?=F0=9F=90=9B=20Fix=20model-free=20apps=20an?= =?UTF-8?q?d=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 9/9] =?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"