added authentification by role in payload

parent 5f097349
...@@ -150,6 +150,46 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} ...@@ -150,6 +150,46 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
[[package]]
name = "bcrypt"
version = "4.1.2"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.7"
files = [
{file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"},
{file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"},
{file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"},
{file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"},
{file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"},
{file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"},
{file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"},
{file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"},
{file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"},
{file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"},
{file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"},
]
[package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2023.11.17" version = "2023.11.17"
...@@ -312,6 +352,25 @@ docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"] ...@@ -312,6 +352,25 @@ docs = ["Sphinx (>=3)", "sphinx-rtd-theme (>=0.2)"]
numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"] numpy = ["numpy (>=1.13.0)", "numpy (>=1.15.0)", "numpy (>=1.18.0)", "numpy (>=1.20.0)"]
tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"] tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytest (>=6)", "pytest-cov (>=2.10.1)", "pytest-isort (>=1.2.0)", "pytest-pycodestyle (>=2)", "pytest-pycodestyle (>=2.2.0)", "pytest-pydocstyle (>=2)", "pytest-pydocstyle (>=2.2.0)", "sphinx (>=3)", "tox (>=3.7.0)"]
[[package]]
name = "dnspython"
version = "2.4.2"
description = "DNS toolkit"
optional = false
python-versions = ">=3.8,<4.0"
files = [
{file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"},
{file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"},
]
[package.extras]
dnssec = ["cryptography (>=2.6,<42.0)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"]
doq = ["aioquic (>=0.9.20)"]
idna = ["idna (>=2.1,<4.0)"]
trio = ["trio (>=0.14,<0.23)"]
wmi = ["wmi (>=1.5.1,<2.0.0)"]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.18.0" version = "0.18.0"
...@@ -330,6 +389,21 @@ six = ">=1.9.0" ...@@ -330,6 +389,21 @@ six = ">=1.9.0"
gmpy = ["gmpy"] gmpy = ["gmpy"]
gmpy2 = ["gmpy2"] gmpy2 = ["gmpy2"]
[[package]]
name = "email-validator"
version = "2.1.0.post1"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
files = [
{file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"},
{file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.0" version = "1.2.0"
...@@ -475,6 +549,26 @@ files = [ ...@@ -475,6 +549,26 @@ files = [
{file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"},
] ]
[[package]]
name = "passlib"
version = "1.7.4"
description = "comprehensive password hashing framework supporting over 30 schemes"
optional = false
python-versions = "*"
files = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
]
[package.dependencies]
bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""}
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]] [[package]]
name = "pyasn1" name = "pyasn1"
version = "0.5.1" version = "0.5.1"
...@@ -510,6 +604,7 @@ files = [ ...@@ -510,6 +604,7 @@ files = [
[package.dependencies] [package.dependencies]
annotated-types = ">=0.4.0" annotated-types = ">=0.4.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.14.5" pydantic-core = "2.14.5"
typing-extensions = ">=4.6.1" typing-extensions = ">=4.6.1"
...@@ -706,6 +801,20 @@ cryptography = ["cryptography (>=3.4.0)"] ...@@ -706,6 +801,20 @@ cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"]
pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"]
[[package]]
name = "python-multipart"
version = "0.0.6"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"},
{file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"},
]
[package.extras]
dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"]
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2023.3.post1" version = "2023.3.post1"
...@@ -840,4 +949,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", ...@@ -840,4 +949,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "9191f5d40c10b0c9370146ca214d8127d45f6f21b080662e4c3b275189e6c25b" content-hash = "34fdb29516e9b3de47fa3d5192c00c708a90c528dac4ce25aeaa37383bac1a11"
...@@ -15,6 +15,9 @@ asyncpg = "^0.29.0" ...@@ -15,6 +15,9 @@ asyncpg = "^0.29.0"
fastapi-pagination = "^0.12.12" fastapi-pagination = "^0.12.12"
httpx = "^0.25.2" httpx = "^0.25.2"
python-jose = {extras = ["cryptography"], version = "^3.3.0"} python-jose = {extras = ["cryptography"], version = "^3.3.0"}
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
python-multipart = "^0.0.6"
pydantic = {extras = ["email"], version = "^2.5.2"}
......
...@@ -4,7 +4,7 @@ import fastapi ...@@ -4,7 +4,7 @@ import fastapi
from .schemas import CategoryBaseSchema from .schemas import CategoryBaseSchema
from .ctrl import CategoryController from .ctrl import CategoryController
from ..dependencies import get_current_user from ..dependencies import get_user_role
router = fastapi.APIRouter(prefix='/categories', tags=['Category']) router = fastapi.APIRouter(prefix='/categories', tags=['Category'])
...@@ -12,19 +12,25 @@ ctrl = CategoryController() ...@@ -12,19 +12,25 @@ ctrl = CategoryController()
@router.get('') @router.get('')
async def get_categories(query: CategoryBaseSchema = fastapi.Depends()): async def get_categories(
query: CategoryBaseSchema = fastapi.Depends(),
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get_list(**query.model_dump(exclude_none=True)) return await ctrl.get_list(**query.model_dump(exclude_none=True))
@router.get('/{id}') @router.get('/{id}')
async def get_category(id: UUID): async def get_category(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get(id) return await ctrl.get(id)
@router.post('') @router.post('')
async def create_category( async def create_category(
body: CategoryBaseSchema, body: CategoryBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.create(**body.model_dump(exclude_none=True)) return await ctrl.create(**body.model_dump(exclude_none=True))
...@@ -32,7 +38,7 @@ async def create_category( ...@@ -32,7 +38,7 @@ async def create_category(
@router.patch('/{id}') @router.patch('/{id}')
async def update_category( async def update_category(
id: UUID, body: CategoryBaseSchema, id: UUID, body: CategoryBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.update(id, **body.model_dump(exclude_none=True)) return await ctrl.update(id, **body.model_dump(exclude_none=True))
...@@ -40,6 +46,6 @@ async def update_category( ...@@ -40,6 +46,6 @@ async def update_category(
@router.delete('/{id}') @router.delete('/{id}')
async def delete_category( async def delete_category(
id: UUID, id: UUID,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.delete(id) return await ctrl.delete(id)
import os
import fastapi as fa import fastapi as fa
from fastapi import Depends, HTTPException, status from fastapi import HTTPException, status
from jose import jwt, JWTError
SECRET_KEY = os.getenv('SECRET_KEY') from .user.services import TokenService, oauth2_scheme
ALGORITHM = os.getenv('ALGORITHM') from db.repositories import UserRepository
from db.models import UserRoleEnum
def get_token(token: str = fa.Header(...)): token_service = TokenService()
return token
user_repo = UserRepository()
def authenticate_user(token: str = Depends(get_token)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try: async def get_user_role(token: str = fa.Security(oauth2_scheme)):
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) token_data = token_service.decode_token(token)
username: str = payload.get("sub")
if username is None:
raise credentials_exception
return username
except JWTError:
raise credentials_exception
if token_data.role == UserRoleEnum.system_consumer:
return await user_repo.get(id=token_data.sub)
def get_current_user(username: str = Depends(authenticate_user)):
if username == "johndoe" or username == 'janedoe':
return username
else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authorized", detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
...@@ -3,7 +3,7 @@ from fastapi import APIRouter ...@@ -3,7 +3,7 @@ from fastapi import APIRouter
from .status.routes import router as status_route from .status.routes import router as status_route
from .type.routes import router as type_route from .type.routes import router as type_route
from .task.routes import router as task_route from .task.routes import router as task_route
from .user.routes import router as user_route from .user.routes import router as user_route, auth_router as auth_route
from .category.routes import router as category_route from .category.routes import router as category_route
...@@ -12,4 +12,5 @@ router.include_router(status_route) ...@@ -12,4 +12,5 @@ router.include_router(status_route)
router.include_router(type_route) router.include_router(type_route)
router.include_router(task_route) router.include_router(task_route)
router.include_router(user_route) router.include_router(user_route)
router.include_router(auth_route)
router.include_router(category_route) router.include_router(category_route)
...@@ -4,7 +4,7 @@ import fastapi ...@@ -4,7 +4,7 @@ import fastapi
from .schemas import StatusBaseSchema from .schemas import StatusBaseSchema
from .ctrl import StatusController from .ctrl import StatusController
from ..dependencies import get_current_user from ..dependencies import get_user_role
router = fastapi.APIRouter(prefix='/statuses', tags=['Status']) router = fastapi.APIRouter(prefix='/statuses', tags=['Status'])
...@@ -12,19 +12,25 @@ ctrl = StatusController() ...@@ -12,19 +12,25 @@ ctrl = StatusController()
@router.get('') @router.get('')
async def get_statuses(query: StatusBaseSchema = fastapi.Depends()): async def get_statuses(
query: StatusBaseSchema = fastapi.Depends(),
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get_list(**query.model_dump(exclude_none=True)) return await ctrl.get_list(**query.model_dump(exclude_none=True))
@router.get('/{id}') @router.get('/{id}')
async def get_status(id: UUID): async def get_status(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get(id) return await ctrl.get(id)
@router.post('') @router.post('')
async def create_status( async def create_status(
body: StatusBaseSchema, body: StatusBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.create(**body.model_dump(exclude_none=True)) return await ctrl.create(**body.model_dump(exclude_none=True))
...@@ -32,7 +38,7 @@ async def create_status( ...@@ -32,7 +38,7 @@ async def create_status(
@router.patch('/{id}') @router.patch('/{id}')
async def update_status( async def update_status(
id: UUID, body: StatusBaseSchema, id: UUID, body: StatusBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.update(id, **body.model_dump(exclude_none=True)) return await ctrl.update(id, **body.model_dump(exclude_none=True))
...@@ -40,6 +46,6 @@ async def update_status( ...@@ -40,6 +46,6 @@ async def update_status(
@router.delete('/{id}') @router.delete('/{id}')
async def delete_status( async def delete_status(
id: UUID, id: UUID,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.delete(id) return await ctrl.delete(id)
...@@ -6,7 +6,7 @@ from .schemas import ( ...@@ -6,7 +6,7 @@ from .schemas import (
TaskPostSchema, TaskPatchSchema, CommentSchema TaskPostSchema, TaskPatchSchema, CommentSchema
) )
from .ctrl import TaskController from .ctrl import TaskController
from ..dependencies import get_current_user from ..dependencies import get_user_role
router = fastapi.APIRouter(prefix='/tasks', tags=['Task']) router = fastapi.APIRouter(prefix='/tasks', tags=['Task'])
...@@ -22,6 +22,7 @@ async def get_tasks( ...@@ -22,6 +22,7 @@ async def get_tasks(
sort_by: str | None = None, sort_by: str | None = None,
page: int = 1, page: int = 1,
size: int = 10, size: int = 10,
current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.get_list( return await ctrl.get_list(
title=title, title=title,
...@@ -34,14 +35,17 @@ async def get_tasks( ...@@ -34,14 +35,17 @@ async def get_tasks(
) )
@router.get('/{id}') @router.get('/{id}')
async def get_task(id: UUID): async def get_task(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get(id) return await ctrl.get(id)
@router.post('') @router.post('')
async def create_task( async def create_task(
body: TaskPostSchema, body: TaskPostSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.create(**body.model_dump(exclude_none=True)) return await ctrl.create(**body.model_dump(exclude_none=True))
...@@ -50,7 +54,7 @@ async def create_task( ...@@ -50,7 +54,7 @@ async def create_task(
async def update_task( async def update_task(
id: UUID, id: UUID,
body: TaskPatchSchema, body: TaskPatchSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.update(id, **body.model_dump(exclude_none=True)) return await ctrl.update(id, **body.model_dump(exclude_none=True))
...@@ -58,13 +62,16 @@ async def update_task( ...@@ -58,13 +62,16 @@ async def update_task(
@router.delete('/{id}') @router.delete('/{id}')
async def delete_task( async def delete_task(
id: UUID, id: UUID,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.delete(id) return await ctrl.delete(id)
@router.get('/{id}/comments') @router.get('/{id}/comments')
async def get_comments(id: UUID): async def get_comments(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get_comments(id) return await ctrl.get_comments(id)
...@@ -72,7 +79,7 @@ async def get_comments(id: UUID): ...@@ -72,7 +79,7 @@ async def get_comments(id: UUID):
async def add_comment( async def add_comment(
id: UUID, id: UUID,
body: CommentSchema, body: CommentSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.add_comment(id, **body.model_dump(exclude_none=True)) return await ctrl.add_comment(id, **body.model_dump(exclude_none=True))
...@@ -80,11 +87,14 @@ async def add_comment( ...@@ -80,11 +87,14 @@ async def add_comment(
@router.delete('/comments/{id}') @router.delete('/comments/{id}')
async def delete_comment( async def delete_comment(
id: UUID, id: UUID,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.delete_comment(id) return await ctrl.delete_comment(id)
@router.get('/tasks/search') @router.get('/tasks/search')
async def search_tasks(keywords: str): async def search_tasks(
keywords: str,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.search(keywords) return await ctrl.search(keywords)
...@@ -4,7 +4,7 @@ import fastapi ...@@ -4,7 +4,7 @@ import fastapi
from .schemas import TypeBaseSchema from .schemas import TypeBaseSchema
from .ctrl import TypeController from .ctrl import TypeController
from ..dependencies import get_current_user from ..dependencies import get_user_role
router = fastapi.APIRouter(prefix='/types', tags=['Type']) router = fastapi.APIRouter(prefix='/types', tags=['Type'])
...@@ -24,7 +24,7 @@ async def get_type(id: UUID): ...@@ -24,7 +24,7 @@ async def get_type(id: UUID):
@router.post('') @router.post('')
async def create_type( async def create_type(
body: TypeBaseSchema, body: TypeBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.create(**body.model_dump(exclude_none=True)) return await ctrl.create(**body.model_dump(exclude_none=True))
...@@ -33,7 +33,7 @@ async def create_type( ...@@ -33,7 +33,7 @@ async def create_type(
async def update_type( async def update_type(
id: UUID, id: UUID,
body: TypeBaseSchema, body: TypeBaseSchema,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.update(id, **body.model_dump(exclude_none=True)) return await ctrl.update(id, **body.model_dump(exclude_none=True))
...@@ -41,6 +41,6 @@ async def update_type( ...@@ -41,6 +41,6 @@ async def update_type(
@router.delete('/{id}') @router.delete('/{id}')
async def delete_type( async def delete_type(
id: UUID, id: UUID,
current_user: str = fastapi.Depends(get_current_user) current_user: str = fastapi.Depends(get_user_role)
): ):
return await ctrl.delete(id) return await ctrl.delete(id)
import os
SECRET_KEY = os.getenv('SECRET_KEY')
ALGORITHM = os.getenv('ALGORITHM')
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES'))
REFRESH_TOKEN_EXPIRE_HOURS = int(os.getenv('REFRESH_TOKEN_EXPIRE_HOURS'))
from uuid import UUID from uuid import UUID
import fastapi import fastapi
from fastapi.security import OAuth2PasswordRequestForm
from .schemas import UserBaseSchema from .schemas import UserCreateSchema, UserUpdateSchema, UserGetSchema
from .ctrl import UserController from .ctrl import UserController
from .services import auth_service, token_service
from ..dependencies import get_user_role
router = fastapi.APIRouter(prefix='/users', tags=['User']) router = fastapi.APIRouter(prefix='/users', tags=['User'])
auth_router = fastapi.APIRouter(prefix='/auth', tags=['Auth'])
ctrl = UserController() ctrl = UserController()
@router.get('') @router.get('')
async def get_users(query: UserBaseSchema = fastapi.Depends()): async def get_users(
query: UserGetSchema = fastapi.Depends(),
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get_list(**query.model_dump(exclude_none=True)) return await ctrl.get_list(**query.model_dump(exclude_none=True))
@router.get('/{id}') @router.get('/{id}')
async def get_user(id: UUID): async def get_user(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.get(id) return await ctrl.get(id)
@router.post('') @router.post('')
async def create_user(body: UserBaseSchema): async def create_user(
body: UserCreateSchema,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.create(**body.model_dump(exclude_none=True)) return await ctrl.create(**body.model_dump(exclude_none=True))
@router.patch('/{id}') @router.patch('/{id}')
async def update_user(id: UUID, body: UserBaseSchema): async def update_user(
id: UUID, body: UserUpdateSchema,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.update(id, **body.model_dump(exclude_none=True)) return await ctrl.update(id, **body.model_dump(exclude_none=True))
@router.delete('/{id}') @router.delete('/{id}')
async def delete_user(id: UUID): async def delete_user(
id: UUID,
current_user: str = fastapi.Depends(get_user_role)
):
return await ctrl.delete(id) return await ctrl.delete(id)
@auth_router.post('/login')
async def login(
form_data: OAuth2PasswordRequestForm = fastapi.Depends(),
):
return await auth_service.login(form_data)
@auth_router.get('/users/me')
async def read_user_me(
user: dict = fastapi.Depends(auth_service.get_current_user)
):
return UserGetSchema.model_validate(user)
@auth_router.post('/refresh')
async def refresh_token(
data: dict,
user: dict = fastapi.Depends(auth_service.get_current_user)
):
return token_service.refresh_token(data['refresh_token'])
from uuid import UUID from uuid import UUID
from dataclasses import dataclass
from datetime import datetime
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pydantic import EmailStr
from db.models import UserRoleEnum
class UserBaseSchema(BaseModel): class UserBaseSchema(BaseModel):
...@@ -16,3 +21,41 @@ class UserIdSchema(BaseModel): ...@@ -16,3 +21,41 @@ class UserIdSchema(BaseModel):
class UserSchema(UserBaseSchema, UserIdSchema): class UserSchema(UserBaseSchema, UserIdSchema):
pass pass
class UserGetSchema(BaseModel):
model_config = ConfigDict(from_attributes=True, use_enum_values=True)
id: UUID | None = None
name: str | None = None
surname: str | None = None
email: EmailStr | None = None
role: UserRoleEnum
class UserCreateSchema(UserBaseSchema):
model_config = ConfigDict(use_enum_values=True)
name: str
surname: str
email: EmailStr
role: UserRoleEnum
class UserUpdateSchema(BaseModel):
model_config = ConfigDict(use_enum_values=True)
name: str | None = None
surname: str | None = None
email: EmailStr | None = None
role: UserRoleEnum | None = None
@dataclass
class TokenData:
sub: int
role: str
exp: datetime | None = None
@dataclass
class LoginRequestModel:
username: str
password: str
from uuid import UUID from uuid import UUID
from datetime import datetime, timedelta
import fastapi as fa
import fastapi.security as fs
from passlib.context import CryptContext
from jose import jwt, JWTError
from db.repositories import UserRepository from db.repositories import UserRepository
from .schemas import UserSchema from .schemas import TokenData, LoginRequestModel, UserGetSchema
from .const import (
SECRET_KEY, ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_HOURS, ALGORITHM
)
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
oauth2_scheme = fs.OAuth2PasswordBearer(tokenUrl='/api/auth/login')
class TokenService:
def create_token(self, data: TokenData, expires_delta: timedelta) -> str:
payload = {
'sub': str(data.sub),
'role': data.role,
'exp': datetime.utcnow() + expires_delta,
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def create_tokens(self, data: TokenData):
access = self.create_token(
data, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
refresh = self.create_token(
data, expires_delta=timedelta(hours=REFRESH_TOKEN_EXPIRE_HOURS)
)
return {"access_token": access, "refresh_token": refresh}
def decode_token(self, token: str) -> TokenData:
credentials_exception = fa.HTTPException(
status_code=fa.status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
decoded_token_payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = decoded_token_payload.get('sub')
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
return TokenData(**decoded_token_payload)
def refresh_token(self, refresh_token: str):
token_data = self.decode_token(refresh_token)
access = self.create_token(token_data, timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
return {'access_token': access, 'refresh_token': refresh_token}
token_service = TokenService()
class AuthService:
def __init__(self) -> None:
self.user_repo = UserRepository()
async def verify_password(self, plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
async def authenticate_user(self, username: str, password: str):
users = await self.user_repo.get_list(username=username)
if users and self.verify_password(password, users[0].password):
return users[0]
async def login(self, data: LoginRequestModel):
user = await self.authenticate_user(username=data.username, password=data.password)
if not user:
raise fa.HTTPException(
status_code=fa.status.HTTP_404_NOT_FOUND,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
return token_service.create_tokens(TokenData(sub=user.id, role=user.role))
async def get_current_user(self, token: str = fa.Security(oauth2_scheme)):
token_data = token_service.decode_token(token)
return await self.user_repo.get(id=token_data.sub)
auth_service = AuthService()
class UserService: class UserService:
...@@ -9,19 +102,19 @@ class UserService: ...@@ -9,19 +102,19 @@ class UserService:
async def get_user_list(self, **kwargs): async def get_user_list(self, **kwargs):
users = await self.user_repository.get_list(**kwargs) users = await self.user_repository.get_list(**kwargs)
return [UserSchema.model_validate(user) for user in users] return [UserGetSchema.model_validate(user) for user in users]
async def get_user(self, id: UUID): async def get_user(self, id: UUID):
user = await self.user_repository.get(id) user = await self.user_repository.get(id)
return UserSchema.model_validate(user) return UserGetSchema.model_validate(user)
async def create_user(self, **kwargs): async def create_user(self, **kwargs):
user = await self.user_repository.create(**kwargs) user = await self.user_repository.create(**kwargs)
return UserSchema.model_validate(user) return UserGetSchema.model_validate(user)
async def update_user(self, id: UUID, **kwargs): async def update_user(self, id: UUID, **kwargs):
user = await self.user_repository.update(id, **kwargs) user = await self.user_repository.update(id, **kwargs)
return UserSchema.model_validate(user) return UserGetSchema.model_validate(user)
async def delete_user(self, id: UUID): async def delete_user(self, id: UUID):
return await self.user_repository.delete(id) return await self.user_repository.delete(id)
...@@ -55,8 +55,18 @@ class Task(BaseModel): ...@@ -55,8 +55,18 @@ class Task(BaseModel):
table = 'tasks' table = 'tasks'
class UserRoleEnum(str, Enum):
admin = "admin"
user = "user"
system_consumer = "system_consumer"
class User(BaseModel): class User(BaseModel):
username = fields.CharField(max_length=100, unique=True) username = fields.CharField(max_length=100, unique=True)
name = fields.CharField(max_length=100)
surname = fields.CharField(max_length=100)
email = fields.CharField(max_length=100)
role = fields.CharEnumField(enum_type=UserRoleEnum, default=UserRoleEnum.user)
password = fields.CharField(max_length=255) password = fields.CharField(max_length=255)
def __str__(self): def __str__(self):
......
...@@ -2,6 +2,7 @@ import math ...@@ -2,6 +2,7 @@ import math
import tortoise import tortoise
from tortoise.expressions import Q from tortoise.expressions import Q
from passlib.context import CryptContext
from .models import ( from .models import (
Status, Type, Task, User, Comment, Category Status, Type, Task, User, Comment, Category
...@@ -98,8 +99,7 @@ class TaskRepository(BaseRepository): ...@@ -98,8 +99,7 @@ class TaskRepository(BaseRepository):
} }
return result return result
async def search(self, keywords: str):
async def search(self, keywords: str):
query = self.model query = self.model
search_terms = keywords.split() search_terms = keywords.split()
...@@ -118,6 +118,15 @@ async def search(self, keywords: str): ...@@ -118,6 +118,15 @@ async def search(self, keywords: str):
class UserRepository(BaseRepository): class UserRepository(BaseRepository):
model = User model = User
@staticmethod
def __get_password_hash(password):
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
return pwd_context.hash(password)
async def create(self, **kwargs):
kwargs['password'] = self.__get_password_hash(kwargs['password'])
return await super().create(**kwargs)
class CommentRepository(BaseRepository): class CommentRepository(BaseRepository):
model = Comment model = Comment
......
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "users" ADD "role" VARCHAR(15) NOT NULL DEFAULT 'user';
ALTER TABLE "users" ADD "surname" VARCHAR(100) NOT NULL;
ALTER TABLE "users" ADD "name" VARCHAR(100) NOT NULL;
ALTER TABLE "users" ADD "email" VARCHAR(100) NOT NULL;"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "users" DROP COLUMN "role";
ALTER TABLE "users" DROP COLUMN "surname";
ALTER TABLE "users" DROP COLUMN "name";
ALTER TABLE "users" DROP COLUMN "email";"""
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment