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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ all: build push

all_arm: build_arm push_arm

prospector:
prospector --profile=prospector.yaml
2 changes: 1 addition & 1 deletion alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from chat.core.config import settings
from chat.database.database import Base
from chat.database.models import User, Chat, Message
from chat.database.models import Chat, Message

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand Down
46 changes: 46 additions & 0 deletions alembic/versions/31d4f3f51824_initial_migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Initial migration

Revision ID: 31d4f3f51824
Revises:
Create Date: 2024-05-21 16:34:16.476888

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '31d4f3f51824'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chat',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('customer_id', sa.Integer(), nullable=False),
sa.Column('shop_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('message',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chat_id', sa.Integer(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['chat_id'], ['chat.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('message')
op.drop_table('chat')
# ### end Alembic commands ###
62 changes: 0 additions & 62 deletions alembic/versions/67e9162f25f9_initial.py

This file was deleted.

13 changes: 1 addition & 12 deletions chat/core/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional, Any, Union, Literal
from typing import Optional, Any

from pydantic import PostgresDsn, field_validator, EmailStr
from pydantic_core.core_schema import ValidationInfo
from pydantic_settings import BaseSettings, SettingsConfigDict

# from pytz import timezone

ENV_PATH = f"{Path(__file__).parent.parent.parent.absolute()}/.env"


Expand All @@ -25,8 +19,3 @@ class Settings(BaseSettings):


settings = Settings()
# TIMEZONE = timezone('Europe/Kyiv')


# def default_time():
# return datetime.now(tz=TIMEZONE)
124 changes: 76 additions & 48 deletions chat/core/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,105 @@
#
from datetime import datetime
from typing import Annotated

from datetime import datetime, timedelta
from typing import Optional, Annotated

import jwt
import requests
from fastapi import Depends
import jwt
from fastapi import Depends, HTTPException
from fastapi.security import (
HTTPBearer,
HTTPAuthorizationCredentials,
)
from jose import JWTError
from requests import RequestException

from chat.core.config import settings
from chat.core.exceptions import IncorrectTokenFormat, Unauthorized, TokenExpired, UserIsNotPresent
from chat.database.crud import get_user, create_user
from chat.database.database import dbDep
from chat.database.models import User
from chat.database.schemas import UserCopy
from chat.core.exceptions import IncorrectTokenFormat, Unauthorized, TokenExpired, NotFound

# Don't use localhost:8080 to avoid slow performance
FLASK_INFO_URL = "http://127.0.0.1:8080/accounts/info"
FLASK_INFO_USER_ID_URL = "http://127.0.0.1:5000/accounts/info/for_chat"
FLASK_INFO_SHOP_ID_URL = "http://127.0.0.1:5000/shops/shop_info/for_chat"
FLASK_INFO_SHOP_OWNER_ID_URL = "http://127.0.0.1:5000/shops/shop_owner_info/for_chat"


auth_scheme = HTTPBearer(scheme_name="TokenScheme", auto_error=True)

session = requests.Session()


def get_user_info(token, user_id):
headers = {"Authorization": f"Bearer {token}"}
response = session.get(FLASK_INFO_URL, headers=headers)
if response.status_code == 200:
return UserCopy(id=user_id, **response.json())
else:
return None


def get_token(token: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)]):
token = token.credentials
def get_token(credentials: Annotated[HTTPAuthorizationCredentials, Depends(auth_scheme)]):
token = credentials.credentials
if "Bearer " in token:
token = token.replace("Bearer ", "")
if token:
return token
raise Unauthorized(detail="Access token is required")


def get_current_user(db: dbDep, token: str = Depends(get_token), ) -> Optional[User]:
def get_user_id_from_service(token: str) -> int:
headers = {"Authorization": f"Bearer {token}"}
response = session.get(FLASK_INFO_USER_ID_URL, headers=headers)
if response.status_code == 200:
data = response.json()
user_id = data.get("user_id")
if user_id is not None:
return user_id
else:
raise IncorrectTokenFormat(detail="User ID not found in the response")
else:
raise Unauthorized(detail="Invalid token or user not authorized")


def get_current_user(token: str = Depends(get_token)):
try:
payload = jwt.decode(token, settings.SECRET, algorithms=["HS256"])
except JWTError as ex:
if str(ex) == "Signature has expired.":
raise IncorrectTokenFormat(detail="Signature has expired")
raise IncorrectTokenFormat(detail="qw")
expire: str = payload.get("exp")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Signature has expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=400, detail="Invalid token format")

expire: int = payload.get("exp")
if expire and int(expire) < datetime.now().timestamp():
raise TokenExpired()

user_id = payload.get("sub")
if not user_id:
raise IncorrectTokenFormat()

user = get_user(db, user_id=user_id)
if not user:
user_info = get_user_info(token, user_id=user_id)
if user_info:
user = create_user(db, user_info)
return user
raise UserIsNotPresent()
if not user.last_check or user.last_check + timedelta(minutes=5) < datetime.now():
print("updated")
user = user.update(get_user_info(token, user_id=user_id))
db.commit()
return user


UserDep: User = Annotated[get_current_user, Depends()]
# Validate token with the external service and get user_id
user_id = get_user_id_from_service(token)
return user_id


UserDep = Annotated[int, Depends(get_current_user)]


def check_shop_id(shop_id: int, token: str = Depends(get_token)):
headers = {"Authorization": f"Bearer {token}"}
try:
response = session.get(f"{FLASK_INFO_SHOP_ID_URL}/{shop_id}", headers=headers)
if response.status_code == 200:
data = response.json()
shop_owner = data.get("shop_owner")
if shop_owner is not None:
return shop_owner
else:
raise NotFound(status_code=404, detail="shop_owner not found in response")
else:
raise NotFound(status_code=404, detail=f"Shop with id {shop_id} not found")
except RequestException as e:
raise HTTPException(status_code=503, detail="Could not connect to the shop service") from e


def get_shop_id_with_token_user(token: str = Depends(get_token)):
headers = {"Authorization": f"Bearer {token}"}
try:
response = session.get(f"{FLASK_INFO_SHOP_OWNER_ID_URL}", headers=headers)
if response.status_code == 200:
data = response.json()
shop_id = data.get("shop_id")
if shop_id is not None:
return shop_id
else:
raise NotFound(status_code=404, detail="shop_id not found in response")
else:
raise NotFound(status_code=404, detail=f"Owner shop not found")
except RequestException as e:
raise HTTPException(status_code=503, detail="Could not connect to the shop service") from e


ShopDep = Annotated[int, Depends(get_shop_id_with_token_user)]
24 changes: 13 additions & 11 deletions chat/customer_router.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
from fastapi import APIRouter
from fastapi.templating import Jinja2Templates
from starlette.requests import Request

from chat.core.dependencies import UserDep
from chat.database.crud import (get_or_create_chat, create_message, get_messages,
get_chats_by_customer_id)
from chat.database.database import dbDep
from chat.database.schemas import MessageSchema, CustomerMessageCreateSchema, ChatsSchema, ChatModelSchema
from chat.database.schemas import MessageSchema, CustomerMessageCreateSchema, ChatModelSchema
from chat.core.exceptions import BadRequest

templates = Jinja2Templates(directory="templates")

router = APIRouter(prefix="/customer", tags=["Customer"])


@router.post("/create_chat")
def create_chat(user: UserDep, db: dbDep, shop_owner_id):
return get_or_create_chat(db=db, customer_id=user.id, shop_owner_id=shop_owner_id)
def create_chat(user_id: UserDep, db: dbDep, shop_id):
if shop_id is not int:
raise BadRequest(status_code=400, detail="shop_id must be an integer")
return get_or_create_chat(db=db, customer_id=user_id, shop_id=shop_id)


@router.post("/send_message", response_model=MessageSchema)
def send_message(user: UserDep, db: dbDep, message: CustomerMessageCreateSchema):
message = create_message(db, customer_id=user.id, shop_owner_id=message.shop_owner_id, message_body=message.body)
def send_message(user_id: UserDep, db: dbDep, message: CustomerMessageCreateSchema):
message = create_message(db, customer_id=user_id, shop_id=message.shop_id, message_body=message.body)
return message


@router.get("/chat/{shop_owner_id}")
def get_chat(user: UserDep, db: dbDep, shop_owner_id: int):
return get_messages(db=db, customer_id=user.id, shop_owner_id=shop_owner_id)
@router.get("/chat/{shop_id}")
def get_chat(user_id: UserDep, db: dbDep, shop_id: int):
return get_messages(db=db, customer_id=user_id, shop_id=shop_id)


@router.get("/chats", response_model=list[ChatModelSchema])
def get_customer_chats(user: UserDep, db: dbDep):
return get_chats_by_customer_id(db, user.id)
def get_customer_chats(user_id: UserDep, db: dbDep):
return get_chats_by_customer_id(db, user_id)
Loading