Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
77 changes: 77 additions & 0 deletions .github/workflows/gh_actions_tests_pre_commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: GitHub Actions for the Skill-Tracker-Auth project
run-name: Skill-Tracker-Auth-Tests
on: [ push, pull_request ]
jobs:
run_tests:
runs-on: ubuntu-latest
env:
#POSTGRES
POSTGRES_DB: default_db
POSTGRES_USER: default_user
POSTGRES_PASSWORD: default_password
POSTGRES_HOST: localhost
POSTGRES_PORT: '5438'
# REDIS
REDIS_PASSWORD: default_password
REDIS_PORT: 6380
REDIS_HOST: localhost
# PASSWORD HASHING
HASHING_ALGORITHM: bcrypt
HASHING_DEPRECATED: auto
# JWT SECRET KEY
KEY: secret_key
ALGORITHM: HS256
REFRESH_LASTING: 3000
ACCESS_LASTING: 300
ADMIN_SECRET: default_secret
# LOCALSTACK
SERVICES: ses,s3
AWS_DEFAULT_REGION: us-east-1
LOCALSTACK_HOST: localstack
DEBUG: 1
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
# LOCAL AWS
AWS_ENDPOINT: http://localhost:4566
RESET_PWD_LENGTH: 10
VERIFICATION_CODE_LENGTH: 5
Comment on lines +7 to +37

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

почему переменные энва прямо в файле хранятся


steps:
- name: Checkout repository code
uses: actions/checkout@v4

- name: Setup Python 3.10
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Create external Docker network
run: docker network create app_network || true

- name: Set up Docker Compose
run: docker compose -f docker-compose.yaml up -d

- name: Set up Docker Compose for localstack
run: docker compose -f docker-compose.localstack.yaml up -d

- name: Wait for DB to be ready
run: |
echo "Waiting for PostgreSQL to be ready..."
until docker exec skills_auth_pg pg_isready -U $POSTGRES_USER; do
sleep 1
done

- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install poetry
poetry config virtualenvs.create false
poetry install
Comment on lines +64 to +69

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

надо ещё покрытие проверять, если < 80, то пайплайн должен падать


- name: Run Tests
run: |
poetry run pytest

- name: Run pre-commit hooks
run: |
poetry run pre-commit
12 changes: 8 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ repos:
rev: 7.2.0
hooks:
- id: flake8
exclude: ^(migrations/|auth_app/models/)
exclude: ^(migrations/)
additional_dependencies:
- flake8-bugbear

Expand All @@ -30,7 +30,7 @@ repos:
hooks:
- id: black
# args: [ "--check", "--diff" ] # set check only
files: ^(auth_app/(repositories|routers|schemes|services)/.*\.py)$
files: ^(auth_app/(repositories|routers|schemes|services)|tests/.*\.py)$
exclude: ^migrations/

- repo: local
Expand All @@ -40,14 +40,18 @@ repos:
entry: pylint
language: system
types: [ python ]
args: [ "-rn", "-sn", "--rcfile=.pylintrc", "--fail-on=I" ]
exclude: ^tests/
args: [
"--rcfile=.pylintrc"
]
# args: [ "-rn", "-sn", "--rcfile=.pylintrc", "--fail-on=I" ]

- repo: https://github.com/pre-commit/mirrors-mypy # static type analysis
rev: v1.15.0
hooks:
- id: mypy
args: [--config-file=pyproject.toml]
exclude: ".*/migrations/.*$|.*/settings.*$|tests|.git.*$|alembic"
exclude: ".*/migrations/.*$|.*/settings.*$|.git.*$|alembic"
require_serial: true

- repo: https://github.com/pycqa/isort # formatter that sorts imports automatically
Expand Down
9 changes: 1 addition & 8 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
[MASTER]
ignore-paths=^\.git.*$,
.*/docker/.*$,
migrations,
.*/migrations/.*$,
.*/tests/.*$,
.*/settings.*$,
.*/base.py,
.*/models/.*$,
ignore=.git,docker,migrations,tests,models
jobs=1

[MESSAGES CONTROL]
Expand Down
Binary file added auth_app/.coverage
Binary file not shown.
7 changes: 7 additions & 0 deletions auth_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,15 @@ class AWSSettings(BaseConfig):
VERIFICATION_CODE_LENGTH: int


class CoreServiceSettings(BaseConfig):
CREATE_USER_URI: str
DELETE_USER_URI: str
SERVICE_SECRET: SecretStr


pg_settings = PostgresSettings()
redis_settings = RedisSettings()
jwt_settings = JWTSettings()
pwd_settings = PasswordSettings()
aws_settings = AWSSettings()
core_service_settings = CoreServiceSettings()
4 changes: 1 addition & 3 deletions auth_app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ async def get_user_service(

async def get_token_service(
session: AsyncSession = Depends(get_db_from_request),
redis: Redis = Depends(get_redis_client),
ses: AioBaseClient = Depends(get_ses_client),
) -> TokenService:
user_repo = UserRepo(session)
token_repo = TokenRepo(session)
return TokenService(user_repo, token_repo, redis, ses)
return TokenService(user_repo, token_repo)
15 changes: 14 additions & 1 deletion auth_app/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from auth_app.exeptions.custom import (
ServiceError,
Expand All @@ -16,6 +17,7 @@
user_verification_exception_handler,
)
from auth_app.messages.common import msg_creator
from auth_app.middleware.cors import cors_settings
from auth_app.middleware.db_session import DBSessionMiddleware
from auth_app.routers.tokens import token_router
from auth_app.routers.users import user_router
Expand All @@ -33,6 +35,13 @@
app.add_exception_handler(TransactionError, transaction_error_handler)

app.add_middleware(DBSessionMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=cors_settings.allow_origins,
allow_credentials=cors_settings.allow_credentials,
allow_methods=cors_settings.allow_methods,
allow_headers=cors_settings.allow_headers
)


@app.on_event("startup")
Expand All @@ -54,4 +63,8 @@ async def root() -> dict:


if __name__ == '__main__':
uvicorn.run('auth_app.main:app')
uvicorn.run(
'auth_app.main:app',
host="localhost",
port=8001,
)
14 changes: 14 additions & 0 deletions auth_app/middleware/cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pydantic import BaseModel


class CORSSettings(BaseModel):
allow_origins: list[str] = [
"http://localhost:8000",
"http://127.0.0.1:8000",
]
allow_credentials: bool = True
allow_methods: list[str] = ["*"]
allow_headers: list[str] = ["*"]


cors_settings = CORSSettings()
62 changes: 62 additions & 0 deletions auth_app/repositories/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from uuid import UUID

from sqlalchemy import (
delete,
select,
update,
)
Expand Down Expand Up @@ -78,3 +79,64 @@ async def update_user(
row = await self.session.execute(stmt)
user_orm = row.scalars().first()
return user_orm

async def delete_user(
self,
user_id: UUID,
) -> UserORM | None:
stmt = (
delete(UserORM)
.where(UserORM.id == user_id)
.returning(UserORM)
)
print(f"stmt: {stmt}")

row = await self.session.execute(stmt)
print(f"row: {row}")
user_orm = row.scalars().first()
return user_orm

async def soft_delete_user(
self,
user_id: UUID,
) -> UserORM:
user = await self.get_user(user_id=user_id)
if user is None:
raise ValueError(f"User with id {user_id} not found")
data = {
"email": "deleted_" + user.email,
"is_active": False,
}
stmt = (
update(UserORM)
.where(UserORM.id == user_id)
.values(**data)
.returning(UserORM)
)

row = await self.session.execute(stmt)
user_orm = row.scalars().first()
return user_orm

async def soft_delete_user_rollback(
self,
user_id: UUID,
) -> UserORM:
user = await self.get_user(user_id=user_id)
if user is None:
raise ValueError(f"User with id {user_id} not found")
email = str(user.email).replace("deleted_", "")
data = {
"email": email,
"is_active": True,
}
stmt = (
update(UserORM)
.where(UserORM.id == user_id)
.values(**data)
.returning(UserORM)
)

row = await self.session.execute(stmt)
user_orm = row.scalars().first()
return user_orm
28 changes: 26 additions & 2 deletions auth_app/routers/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
APIRouter,
Body,
Depends,
Header,
HTTPException,
status,
)
Expand All @@ -13,6 +14,7 @@
GetAccessScheme,
GetRefreshScheme,
RoleDataScheme,
VerifyAccessScheme,
)
from auth_app.schemes.users import (
AuthUserScheme,
Expand All @@ -23,6 +25,7 @@
from auth_app.services.utils.token_handler import (
TokenData,
get_current_token_payload,
get_current_token_payload_for_exchange,
)

token_router = APIRouter(
Expand Down Expand Up @@ -76,7 +79,7 @@ async def create_refresh(
status_code=status.HTTP_201_CREATED,
)
async def exchange_refresh(
token_data: TokenData = Depends(get_current_token_payload),
token_data: TokenData = Depends(get_current_token_payload_for_exchange),
token_service: TokenService = Depends(get_token_service),
) -> GetRefreshScheme:
token = await token_service.exchange_refresh_token(
Expand All @@ -88,7 +91,7 @@ async def exchange_refresh(
@token_router.post(
path='/access/create',
response_model=GetAccessScheme,
description='Generate access token for the user',
description='Generate new access token for the user',
status_code=status.HTTP_201_CREATED,
)
async def create_access(
Expand All @@ -99,3 +102,24 @@ async def create_access(
token_data=token_data,
)
return GetAccessScheme(message=token)


@token_router.get(
path='/access/verify/',
response_model=VerifyAccessScheme,
description='Verify the access token',
status_code=status.HTTP_200_OK,
)
async def verify_access(
auth_data: Annotated[str, Header(alias="Authorization")],
token_service: TokenService = Depends(get_token_service),
) -> VerifyAccessScheme:
if not auth_data.startswith("Bearer"):
raise HTTPException(
detail="Invalid Authorization header",
status_code=status.HTTP_401_UNAUTHORIZED,
)
token_data = await token_service.verify_access_token(
auth_data=auth_data,
)
return token_data
28 changes: 28 additions & 0 deletions auth_app/routers/users.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Annotated
from uuid import UUID

from fastapi import (
APIRouter,
Expand All @@ -13,6 +14,7 @@
from auth_app.schemes.users import (
CreateResponseScheme,
CreateUserExtendedScheme,
DeleteUserScheme,
GetUserScheme,
MessageResponseScheme,
UserFilterScheme,
Expand Down Expand Up @@ -148,3 +150,29 @@ async def get_users(
detail='Relevant users not found',
)
return [GetUserScheme.model_validate(user) for user in users]


@user_router.delete(
path='/{user_id}',
response_model=GetUserScheme,
description="Delete the user",
status_code=status.HTTP_200_OK,
)
async def delete_user(
user_id: UUID,
token_data: TokenData = Depends(get_current_token_payload),
user_service: UserService = Depends(get_user_service),
) -> GetUserScheme:
delete_model = DeleteUserScheme(
id=user_id,
)
deleted_user = await user_service.delete_user_record(
token_data=token_data,
delete_model=delete_model,
)
if not deleted_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='User not found or already deleted',
)
return GetUserScheme.model_validate(deleted_user)
Loading