diff --git a/docs/apps.md b/docs/apps.md index dc70830..fdcf5c4 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` 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. diff --git a/pyproject.toml b/pyproject.toml index 6db719c..094c428 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/ohmyapi/__init__.py b/src/ohmyapi/__init__.py index 4191a45..dfda844 100644 --- a/src/ohmyapi/__init__.py +++ b/src/ohmyapi/__init__.py @@ -1 +1 @@ -__VERSION__ = "0.6.2" +__VERSION__ = "0.5.6" diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index b9785c7..2d58ce1 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -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"))] diff --git a/src/ohmyapi/builtin/auth/routes.py b/src/ohmyapi/builtin/auth/routes.py index fa2d4e7..0104a4e 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 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="", ) diff --git a/src/ohmyapi/core/runtime.py b/src/ohmyapi/core/runtime.py index dea8d78..8fedc3c 100644 --- a/src/ohmyapi/core/runtime.py +++ b/src/ohmyapi/core/runtime.py @@ -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(), } diff --git a/src/ohmyapi/middleware/cors.py b/src/ohmyapi/middleware/cors.py index 77d49b6..49852cd 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), - } -) + }), +] +