Compare commits

..

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

7 changed files with 61 additions and 43 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` 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. 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.6.2" version = "0.5.5"
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.6.2" __VERSION__ = "0.5.5"

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

@ -6,9 +6,8 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Request, st
from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.security.utils import get_authorization_scheme_param from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel 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 jwt
import settings import settings
@ -53,6 +52,7 @@ class Claims(BaseModel):
type: str type: str
sub: str sub: str
user: ClaimsUser user: ClaimsUser
roles: List[str]
exp: str exp: str
@ -79,7 +79,7 @@ class TokenType(str, Enum):
refresh = "refresh" refresh = "refresh"
def claims(token_type: TokenType, user: User = []) -> Claims: def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims:
return Claims( return Claims(
type=token_type, type=token_type,
sub=str(user.id), sub=str(user.id),
@ -88,6 +88,7 @@ def claims(token_type: TokenType, user: User = []) -> Claims:
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="",
) )
@ -213,11 +214,7 @@ async def refresh_token(refresh_token: TokenRefresh = Body(...)):
) )
user_id = payload.get("sub") user_id = payload.get("sub")
try: user = await User.get(id=user_id)
user = await User.get(id=user_id)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"

View file

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