add authentication system and missing frontend routes #1

Merged
jlobbes merged 5 commits from auth into main 2023-06-01 00:22:38 +01:00
18 changed files with 172 additions and 80 deletions
Showing only changes of commit 3c4a7bacdf - Show all commits

View File

@ -8,7 +8,8 @@ from todo.models import todos as todomodel
from todo.models import users as usermodel from todo.models import users as usermodel
from todo.schemas import todos as todoschema from todo.schemas import todos as todoschema
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
from todo.models.common import SortOrder from todo.dependencies.common import SortOrder
from todo.dependencies.todos import SortableTodoItemField
def create_todo(db: Session, todo: todoschema.TodoItemCreate, user_id: int) -> todoschema.TodoItem: def create_todo(db: Session, todo: todoschema.TodoItemCreate, user_id: int) -> todoschema.TodoItem:
@ -44,7 +45,7 @@ def read_todos_for_user(
db: Session, db: Session,
user_id: int, user_id: int,
skip: int = 0, limit: int = 100, skip: int = 0, limit: int = 100,
sortby: todomodel.SortableTodoItemField = todomodel.SortableTodoItemField('updated'), sortby: SortableTodoItemField = SortableTodoItemField('updated'),
sortorder: SortOrder = SortOrder['desc'], sortorder: SortOrder = SortOrder['desc'],
) -> list[todoschema.TodoItem]: ) -> list[todoschema.TodoItem]:
"""Returns a range of todo-items of the user with the specified user_id from the database.""" """Returns a range of todo-items of the user with the specified user_id from the database."""

View File

@ -6,7 +6,8 @@ from sqlalchemy.orm import Session
from todo.models import users as usermodel from todo.models import users as usermodel
from todo.schemas import users as userschema from todo.schemas import users as userschema
from todo.models.common import SortOrder from todo.dependencies.common import SortOrder
from todo.dependencies.users import SortableUserField
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
@ -60,7 +61,7 @@ def read_user_by_email(db: Session, email: str) -> userschema.User:
def read_users( def read_users(
db: Session, db: Session,
skip: int = 0, limit: int = 100, skip: int = 0, limit: int = 100,
sortby: usermodel.SortableUserField = usermodel.SortableUserField('id'), sortby: SortableUserField = SortableUserField('id'),
sortorder: SortOrder = SortOrder['asc'], sortorder: SortOrder = SortOrder['asc'],
) -> list[userschema.User]: ) -> list[userschema.User]:
"""Returns an range of users from the database.""" """Returns an range of users from the database."""

View File

@ -0,0 +1,43 @@
"""This module provides FastAPI dependencies for commonly used query parameters."""
from enum import Enum
from typing import Callable
import sqlalchemy
from todo.database.engine import Base
class SortOrder(Enum):
"""Possible sort orders for database queries."""
asc = 'asc'
desc = 'desc'
@property
def call(self) -> Callable:
"""Returns the sqlalchemy sort function depending on the instance value."""
if self.value == 'asc':
return sqlalchemy.asc
elif self.value == 'desc':
return sqlalchemy.desc
else:
raise RuntimeError("Logic error.")
class PaginationParams():
"""Represents query parameters used for pagination, when querying for a list of items.
Attributes
----------
skip : int
The number of items to be skipped from the start of the start of the list.
limit : int
Limits the total number of returned list items to the specified number.
"""
def __init__(self, skip: int = 0, limit: int = 100):
self.skip = skip
self.limit = limit

View File

@ -0,0 +1,35 @@
"""Query parameters used to sort todo-items."""
from enum import Enum
from sqlalchemy import Column
from todo.models.todos import TodoItem
from todo.dependencies.common import PaginationParams, SortOrder
class SortableTodoItemField(Enum):
"""Defines which fields todo-item lists can be sorted on."""
id = 'id'
title = 'title'
done = 'done'
created = 'created'
updated = 'updated'
finished = 'finished'
@property
def field(self) -> Column:
return getattr(TodoItem, self.value)
class TodoItemSortablePaginationParams(PaginationParams):
def __init__(
self,
skip: int = 0, limit: int = 100,
sortby: SortableTodoItemField = SortableTodoItemField['updated'],
sortorder: SortOrder = SortOrder['desc'],
):
super().__init__(skip=skip, limit=limit)
self.sortby = sortby
self.sortorder = sortorder

View File

@ -0,0 +1,35 @@
"""Query parameters used to sort users."""
from enum import Enum
from sqlalchemy import Column
from todo.models.users import User
from todo.dependencies.common import PaginationParams, SortOrder
class SortableUserField(Enum):
"""Defines which fields user lists can be sorted on."""
id = 'id'
email = 'email'
created = 'created'
updated = 'updated'
first_name = 'first_name'
last_name = 'last_name'
@property
def field(self) -> Column:
return getattr(User, self.value)
class UserSortablePaginationParams(PaginationParams):
def __init__(
self,
skip: int = 0, limit: int = 100,
sortby: SortableUserField = SortableUserField['id'],
sortorder: SortOrder = SortOrder['asc'],
):
super().__init__(skip=skip, limit=limit)
self.sortby = sortby
self.sortorder = sortorder

View File

@ -1,26 +0,0 @@
"""This module contains common utilities for handling models."""
import enum
from typing import Callable
import sqlalchemy
class SortOrder(enum.Enum):
"""Possible sort orders for database queries."""
asc = 'asc'
ASC = 'asc'
desc = 'desc'
DESC = 'desc'
@property
def call(self) -> Callable:
"""Returns the sqlalchemy sort function depending on the instance value."""
if self.value == 'asc':
return sqlalchemy.asc
elif self.value == 'desc':
return sqlalchemy.desc
else:
raise RuntimeError("Logic error.")

View File

@ -1,8 +1,6 @@
"""This module defines the SQL data model for todo items.""" """This module defines the SQL data model for todo items."""
import enum from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Boolean
from sqlalchemy import Column, ForeignKey, Integer, String, DateTime, Date, Enum, CheckConstraint, Boolean
from sqlalchemy.sql.functions import now from sqlalchemy.sql.functions import now
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -26,18 +24,3 @@ class TodoItem(Base):
user_id = Column('user_id', Integer, ForeignKey('users.id', ondelete="CASCADE"), nullable=False, index=True) user_id = Column('user_id', Integer, ForeignKey('users.id', ondelete="CASCADE"), nullable=False, index=True)
user = relationship(User, back_populates="todo_items", uselist=False) user = relationship(User, back_populates="todo_items", uselist=False)
class SortableTodoItemField(enum.Enum):
"""Defines which fields todo-item lists can be sorted on."""
id = 'id'
title = 'title'
done = 'done'
created = 'created'
updated = 'updated'
finished = 'finished'
@property
def field(self) -> Column:
return getattr(TodoItem, self.value)

View File

@ -1,7 +1,5 @@
"""This module defines the SQL data model for users.""" """This module defines the SQL data model for users."""
import enum
from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql.functions import now from sqlalchemy.sql.functions import now
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -23,18 +21,3 @@ class User(Base):
last_name = Column('last_name', String, nullable=False, index=True) last_name = Column('last_name', String, nullable=False, index=True)
todo_items = relationship("TodoItem", back_populates="user", uselist=True, cascade="all, delete") todo_items = relationship("TodoItem", back_populates="user", uselist=True, cascade="all, delete")
class SortableUserField(enum.Enum):
"""Defines which fields user lists can be sorted on."""
id = 'id'
email = 'email'
created = 'created'
updated = 'updated'
first_name = 'first_name'
last_name = 'last_name'
@property
def field(self) -> Column:
return getattr(User, self.value)

View File

@ -1,5 +1,7 @@
"""This module contains endpoints for operations related to users.""" """This module contains endpoints for operations related to users."""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -8,12 +10,11 @@ from todo.schemas import todos as todoschema
from todo.crud import todos as todocrud from todo.crud import todos as todocrud
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
from todo.utils.exceptions import create_exception_dict as fmt from todo.utils.exceptions import create_exception_dict as fmt
from todo.models.todos import SortableTodoItemField from todo.dependencies.todos import TodoItemSortablePaginationParams
from todo.models.common import SortOrder
router = APIRouter( router = APIRouter(
prefix="/todo", prefix="/todos",
tags=["todo-items"] tags=["todo-items"]
) )
@ -42,13 +43,16 @@ def read_todo(todo_id: int, db: Session = Depends(get_db)):
@router.get("/user/{user_id}", response_model=list[todoschema.TodoItem]) @router.get("/user/{user_id}", response_model=list[todoschema.TodoItem])
def read_todos( def read_todos(
user_id: int, user_id: int,
skip: int = 0, limit: int = 100, commons: Annotated[TodoItemSortablePaginationParams, Depends(TodoItemSortablePaginationParams)],
sortby: SortableTodoItemField = SortableTodoItemField['updated'],
sortorder: SortOrder = SortOrder['desc'],
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
try: try:
return todocrud.read_todos_for_user(db=db, user_id=user_id, skip=skip, limit=limit, sortby=sortby, sortorder=sortorder) return todocrud.read_todos_for_user(
db=db,
user_id=user_id,
skip=commons.skip, limit=commons.limit,
sortby=commons.sortby, sortorder=commons.sortorder,
)
except InvalidFilterParameterException as e: except InvalidFilterParameterException as e:
raise HTTPException(400, fmt(str(e))) raise HTTPException(400, fmt(str(e)))
except NotFoundException as e: except NotFoundException as e:

View File

@ -1,5 +1,7 @@
"""This module contains endpoints for operations related to users.""" """This module contains endpoints for operations related to users."""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -8,8 +10,7 @@ from todo.schemas import users as userschema
from todo.crud import users as usercrud from todo.crud import users as usercrud
from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException from todo.utils.exceptions import NotFoundException, InvalidFilterParameterException
from todo.utils.exceptions import create_exception_dict as fmt from todo.utils.exceptions import create_exception_dict as fmt
from todo.models.common import SortOrder from todo.dependencies.users import UserSortablePaginationParams
from todo.models.users import SortableUserField
router = APIRouter( router = APIRouter(
@ -43,13 +44,17 @@ def read_user(id: int, db: Session = Depends(get_db)):
@router.get("/", response_model=list[userschema.User]) @router.get("/", response_model=list[userschema.User])
def read_users( def read_users(
skip: int = 0, limit: int = 100, commons: Annotated[UserSortablePaginationParams, Depends(UserSortablePaginationParams)],
sortby: SortableUserField = SortableUserField['id'],
sortorder: SortOrder = SortOrder['asc'],
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
try: try:
return usercrud.read_users(db=db, skip=skip, limit=limit, sortby=sortby, sortorder=sortorder) return usercrud.read_users(
db=db,
skip=commons.skip,
limit=commons.limit,
sortby=commons.sortby,
sortorder=commons.sortorder
)
except InvalidFilterParameterException as e: except InvalidFilterParameterException as e:
raise HTTPException(400, fmt(str(e))) raise HTTPException(400, fmt(str(e)))

View File

@ -0,0 +1,12 @@
"""This module contains common functions for schema validation."""
def is_valid_id(id: int) -> bool:
"""Checks whether the specified id is a valid ID.
Performs a shallow check on whether the ID is a valid primary key.
"""
if not isinstance(id, int):
raise TypeError(f"Expected an integer but got: {type(id)}")
return id > 0

View File

@ -6,6 +6,8 @@ from typing import Optional
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from todo.schemas.common import is_valid_id
class AbstractTodoItemValidator(BaseModel, ABC): class AbstractTodoItemValidator(BaseModel, ABC):
"""Base class for todo-item validators shared with child classes.""" """Base class for todo-item validators shared with child classes."""
@ -55,5 +57,11 @@ class TodoItem(AbstractTodoItem):
updated: datetime updated: datetime
finished: datetime | None finished: datetime | None
@validator('id')
def assert_id_is_valid(cls, id):
if not is_valid_id(id):
raise ValueError("ID is invalid.")
return id
class Config: class Config:
orm_mode = True orm_mode = True

View File

@ -7,6 +7,8 @@ from re import compile
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from todo.schemas.common import is_valid_id
def is_valid_email_format(input_str: str) -> bool: def is_valid_email_format(input_str: str) -> bool:
"""Checks whether the input string is a valid email address format. """Checks whether the input string is a valid email address format.
@ -106,5 +108,11 @@ class User(AbstractUser):
created: datetime created: datetime
updated: datetime updated: datetime
@validator('id')
def assert_id_is_valid(cls, id):
if not is_valid_id(id):
raise ValueError("ID is invalid.")
return id
class Config: class Config:
orm_mode = True orm_mode = True

View File

@ -44,8 +44,8 @@
const table = { const table = {
caption: `List of TODOs for user ${data.user.first_name} ${data.user.last_name}`, caption: `List of TODOs for user ${data.user.first_name} ${data.user.last_name}`,
columns: cols, columns: cols,
itemsEndpoint: `/api/todo/user/${data.user.id}/`, itemsEndpoint: `/api/todos/user/${data.user.id}/`,
itemCountEndpoint: `/api/todo/user/${data.user.id}/total/`, itemCountEndpoint: `/api/todos/user/${data.user.id}/total/`,
}; };
</script> </script>