Compare commits

..

No commits in common. "main" and "0.5.6" have entirely different histories.
main ... 0.5.6

7 changed files with 60 additions and 37 deletions

View file

@ -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` model and provides API endpoints for JWT authentication.
For example, OhMyAPI comes bundled with an `auth` app that carries a `User` and `Group` model and provides API endpoints for JWT authentication.
Apps help organizing projects by isolating individual components (or "features") from one another.

View file

@ -1,6 +1,6 @@
[project]
name = "ohmyapi"
version = "0.6.2"
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"]

View file

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

View file

@ -1,16 +1,27 @@
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)
@ -18,22 +29,20 @@ 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)
created_at: datetime = field.DatetimeField(auto_now_add=True)
updated_at: datetime = field.DatetimeField(auto_now=True)
groups: field.ManyToManyRelation[Group] = field.ManyToManyField(
"ohmyapi_auth.Group",
related_name="users",
through="ohmyapi_auth.UserGroups",
forward_key="user_id",
backward_key="group_id",
)
class Schema:
include = {
"id",
"username",
"is_admin",
"is_staff"
"created_at",
"updated_at",
}
exclude = ["password_hash", "email_hash"]
def __str__(self):
fields = {
'username': self.username,
'username': self.username if self.username else "-",
'is_admin': 'y' if self.is_admin else 'n',
'is_staff': 'y' if self.is_staff else 'n',
}
@ -58,3 +67,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"))]

View file

@ -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 User
from ohmyapi.builtin.auth.models import Group, User
import jwt
import settings
@ -53,6 +53,7 @@ class Claims(BaseModel):
type: str
sub: str
user: ClaimsUser
roles: List[str]
exp: str
@ -79,7 +80,7 @@ class TokenType(str, Enum):
refresh = "refresh"
def claims(token_type: TokenType, user: User = []) -> Claims:
def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims:
return Claims(
type=token_type,
sub=str(user.id),
@ -88,6 +89,7 @@ def claims(token_type: TokenType, user: User = []) -> Claims:
is_admin=user.is_admin,
is_staff=user.is_staff,
),
roles=[g.name for g in groups],
exp="",
)

View file

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