Compare commits

..

5 commits
0.5.6 ... main

Author SHA1 Message Date
Brian Wiborg
b5691f3133
🔖 0.6.2 2025-11-05 21:37:34 +01:00
Brian Wiborg
5f80a7a86f
🐛 Fix model-free apps and middleware installer 2025-11-05 21:29:25 +01:00
Brian Wiborg
b588ebcf8a
🚑️ Remove roles claim 2025-10-28 14:45:18 +01:00
Brian Wiborg
a9b88d87d6
🔖 0.6.0 2025-10-28 14:40:29 +01:00
Brian Wiborg
7163fe778e
♻️ Refactor ohmyapi_auth
- remove Group and UserGroups
  (should be handled by dedicated app, if even)
- enforce User.Schema() include-fields
2025-10-28 14:39:42 +01:00
7 changed files with 37 additions and 60 deletions

View file

@ -1,7 +1,7 @@
# Apps # Apps
Apps are a way to group database models and API routes that contextually belong together. 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. Apps help organizing projects by isolating individual components (or "features") from one another.

View file

@ -1,6 +1,6 @@
[project] [project]
name = "ohmyapi" name = "ohmyapi"
version = "0.5.6" version = "0.6.2"
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
license = "MIT" license = "MIT"
keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"] keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"]

View file

@ -1 +1 @@
__VERSION__ = "0.5.6" __VERSION__ = "0.6.2"

View file

@ -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.db import Model, field, Q
from ohmyapi.router import HTTPException from ohmyapi.router import HTTPException
from .utils import hmac_hash 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") 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): class User(Model):
id: UUID = field.data.UUIDField(pk=True) id: UUID = field.data.UUIDField(pk=True)
username: str = field.CharField(max_length=150, unique=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) password_hash: str = field.CharField(max_length=128)
is_admin: bool = field.BooleanField(default=False) is_admin: bool = field.BooleanField(default=False)
is_staff: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False)
groups: field.ManyToManyRelation[Group] = field.ManyToManyField( created_at: datetime = field.DatetimeField(auto_now_add=True)
"ohmyapi_auth.Group", updated_at: datetime = field.DatetimeField(auto_now=True)
related_name="users",
through="ohmyapi_auth.UserGroups",
forward_key="user_id",
backward_key="group_id",
)
class Schema: class Schema:
exclude = ["password_hash", "email_hash"] include = {
"id",
"username",
"is_admin",
"is_staff"
"created_at",
"updated_at",
}
def __str__(self): def __str__(self):
fields = { fields = {
'username': self.username if self.username else "-", 'username': self.username,
'is_admin': 'y' if self.is_admin else 'n', 'is_admin': 'y' if self.is_admin else 'n',
'is_staff': 'y' if self.is_staff 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): if user and user.verify_password(password):
return user return user
return None 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"))]

View file

@ -8,7 +8,7 @@ from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist from tortoise.exceptions import DoesNotExist
from ohmyapi.builtin.auth.models import Group, User from ohmyapi.builtin.auth.models import User
import jwt import jwt
import settings import settings
@ -53,7 +53,6 @@ class Claims(BaseModel):
type: str type: str
sub: str sub: str
user: ClaimsUser user: ClaimsUser
roles: List[str]
exp: str exp: str
@ -80,7 +79,7 @@ class TokenType(str, Enum):
refresh = "refresh" refresh = "refresh"
def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims: def claims(token_type: TokenType, user: User = []) -> Claims:
return Claims( return Claims(
type=token_type, type=token_type,
sub=str(user.id), sub=str(user.id),
@ -89,7 +88,6 @@ def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claim
is_admin=user.is_admin, is_admin=user.is_admin,
is_staff=user.is_staff, is_staff=user.is_staff,
), ),
roles=[g.name for g in groups],
exp="", exp="",
) )

View file

@ -342,9 +342,9 @@ class App:
except ModuleNotFoundError: except ModuleNotFoundError:
return return
getter = getattr(mod, "get", None) installer = getattr(mod, "install", None)
if getter is not None: if installer is not None:
for middleware in getter(): for middleware in installer():
self._middlewares.append(middleware) self._middlewares.append(middleware)
def __serialize_route(self, route): def __serialize_route(self, route):
@ -404,10 +404,15 @@ class App:
""" """
Convenience method for serializing the runtime data. 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 { return {
"models": [ "models": models,
f"{self.name}.{m.__name__}" for m in self._models[f"{self.name}.models"]
],
"middlewares": self.__serialize_middleware(), "middlewares": self.__serialize_middleware(),
"routes": self.__serialize_router(), "routes": self.__serialize_router(),
} }

View file

@ -15,12 +15,12 @@ CORS_CONFIG: Dict[str, Any] = getattr(settings, "MIDDLEWARE_CORS", {})
if not isinstance(CORS_CONFIG, dict): if not isinstance(CORS_CONFIG, dict):
raise ValueError("MIDDLEWARE_CORS must be of type dict") raise ValueError("MIDDLEWARE_CORS must be of type dict")
middleware = [ middleware = (
(CORSMiddleware, { CORSMiddleware,
{
"allow_origins": CORS_CONFIG.get("ALLOW_ORIGINS", DEFAULT_ORIGINS), "allow_origins": CORS_CONFIG.get("ALLOW_ORIGINS", DEFAULT_ORIGINS),
"allow_credentials": CORS_CONFIG.get("ALLOW_CREDENTIALS", DEFAULT_CREDENTIALS), "allow_credentials": CORS_CONFIG.get("ALLOW_CREDENTIALS", DEFAULT_CREDENTIALS),
"allow_methods": CORS_CONFIG.get("ALLOW_METHODS", DEFAULT_METHODS), "allow_methods": CORS_CONFIG.get("ALLOW_METHODS", DEFAULT_METHODS),
"allow_headers": CORS_CONFIG.get("ALLOW_HEADERS", DEFAULT_HEADERS), "allow_headers": CORS_CONFIG.get("ALLOW_HEADERS", DEFAULT_HEADERS),
}), }
] )