This commit is contained in:
2024-03-03 07:22:42 +03:00
parent 804b658c6e
commit d870f1cffe
27 changed files with 303 additions and 78 deletions

33
auth/jwt.py Normal file
View File

@@ -0,0 +1,33 @@
from typing import Annotated
from fastapi import HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy import select
from starlette import status
from backend import config
from database import User
from jose import jwt
from database.base import DatabaseDependency
oauth2_scheme = OAuth2PasswordBearer("")
ALGORITHM = "HS256"
def generate_jwt_token(user: User):
return jwt.encode({'sub': user.id}, settings.SECRET_KEY, algorithm=ALGORITHM)
def require_jwt_sub(token: Annotated[str, Depends(oauth2_scheme)]):
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid authorization credentials')
return payload
async def get_current_user(db_session: DatabaseDependency, user_id: Annotated[int, Depends(require_jwt_sub)]) -> User:
user = await db_session.scalar(select(User).where(User.id == user_id))
if user:
return user

View File

@@ -1,8 +1,7 @@
import hmac
import hashlib
import os
import settings
from backend import config
def _generate_hash(telegram_data: dict):

View File

@@ -13,3 +13,5 @@ PG_HOST = os.environ.get('PG_HOST')
# Telegram
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN')
SECRET_KEY = os.environ.get('SECRET_KEY')

19
backend/session.py Normal file
View File

@@ -0,0 +1,19 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from .config import PG_DATABASE, PG_HOST, PG_PASSWORD, PG_LOGIN
DATABASE_URL = f'postgresql+asyncpg://{PG_LOGIN}:{PG_PASSWORD}@{PG_HOST}/{PG_DATABASE}'
engine = create_async_engine(DATABASE_URL)
session_maker = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False
)
async def get_session() -> AsyncSession:
async with session_maker() as session:
yield session

View File

@@ -1 +0,0 @@
from .models.basic import *

View File

@@ -1,24 +0,0 @@
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
from settings import PG_LOGIN, PG_PASSWORD, PG_HOST, PG_DATABASE
DATABASE_URL = f'postgresql+asyncpg://{PG_LOGIN}:{PG_PASSWORD}@{PG_HOST}/{PG_DATABASE}'
engine = create_async_engine(DATABASE_URL)
session_maker = sessionmaker(engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False)
BaseModel = declarative_base()
async def get_session() -> AsyncSession:
async with session_maker() as session:
yield session
DatabaseDependency = Annotated[AsyncSession, Depends(get_session)]

View File

@@ -1 +0,0 @@
from .basic import User

20
main.py
View File

@@ -1,28 +1,14 @@
from typing import Annotated
from fastapi import FastAPI
from fastapi import FastAPI, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.base import get_session
from database.models import User
import routers
app = FastAPI()
routers_list = [
routers.auth_router
routers.auth_router,
routers.deal_router
]
for router in routers_list:
app.include_router(router)
@app.get("/")
async def root(db_session: Annotated[AsyncSession, Depends(get_session)]):
user: User = await db_session.scalar(select(User).where(User.id == 1))
return {"message": user.login}
@app.get("/hello/{name}")
async def say_hello(name: str):
return {"message": f"Hello {name}"}

View File

@@ -1,15 +1,13 @@
import asyncio
import backend.config as settings
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
import settings
from database.base import engine, BaseModel
from database.models import *
from backend.session import engine
from models import BaseModel
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.

2
models/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .auth import *
from .deal import *

View File

@@ -1,6 +1,6 @@
from sqlalchemy import Column, Integer, String, BigInteger, Boolean
from sqlalchemy import Column, Integer, BigInteger, String, Boolean
from ..base import BaseModel
from models.base import BaseModel
class User(BaseModel):

3
models/base.py Normal file
View File

@@ -0,0 +1,3 @@
from sqlalchemy.orm import declarative_base
BaseModel = declarative_base()

41
models/deal.py Normal file
View File

@@ -0,0 +1,41 @@
from enum import IntEnum, unique
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from models.base import BaseModel
@unique
class DealStatus(IntEnum):
AWAITING_ACCEPTANCE = 0
PACKAGING = 1
AWAITING_SHIPMENT = 2
AWAITING_PAYMENT = 3
COMPLETED = 4
CANCELLED = 5
class Deal(BaseModel):
__tablename__ = 'deals'
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
name = Column(String, nullable=False, comment='Название сделки')
created_at = Column(DateTime, nullable=False, comment='Дата создания')
current_status = Column(Integer, nullable=False, comment='Текущий статус')
status_history = relationship('DealStatusHistory', back_populates='deal')
class DealStatusHistory(BaseModel):
__tablename__ = 'deals_status_history'
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID сделки')
deal = relationship('Deal', back_populates='status_history')
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user = relationship('User')
changed_at = Column(DateTime, nullable=False, comment='Дата и время когда произошла смена статуса')
from_status = Column(Integer, nullable=False, comment='Предыдущий статус')
to_status = Column(Integer, nullable=False, comment='Новый статус')

View File

@@ -1 +1,3 @@
from .auth import auth_router
from .deal import deal_router

View File

@@ -1,12 +1,11 @@
from fastapi import APIRouter
from sqlalchemy import select, insert
from typing import Annotated
import database
from database import User
from database.base import DatabaseDependency
from schemas.auth.requests import *
from auth.telegram import telegram_authorize
from schemas.auth.responses import AuthLoginResponse
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.auth import AuthLoginRequest, AuthLoginResponse
from services.auth import AuthService
auth_router = APIRouter(
prefix='/auth',
@@ -15,6 +14,5 @@ auth_router = APIRouter(
@auth_router.post('/login', response_model=AuthLoginResponse)
async def login(request: AuthLoginRequest, db_session: DatabaseDependency):
existing_user: User = await db_session.scalar(select(User).where(User.telegram_id == request.id))
return AuthLoginResponse(ok=True, jwt_token="dasdasd")
async def login(request: AuthLoginRequest, session: Annotated[AsyncSession, Depends(get_session)]):
return await AuthService(session).authenticate(request)

32
routers/deal.py Normal file
View File

@@ -0,0 +1,32 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from models import User
from schemas.deal import *
from services.auth import get_current_user
from services.deal import DealService
deal_router = APIRouter(
prefix='/deal',
tags=['deal']
)
@deal_router.post('/create')
async def create(
request: DealCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await DealService(session).create(request)
@deal_router.post('/changeStatus', response_model=DealChangeStatusResponse)
async def change_status(
request: DealChangeStatusRequest,
session: Annotated[AsyncSession, Depends(get_session)],
user: Annotated[User, Depends(get_current_user)]
):
return await DealService(session).change_status(request, user)

13
schemas/auth.py Normal file
View File

@@ -0,0 +1,13 @@
from schemas.base import CustomModel
class AuthLoginRequest(CustomModel):
auth_date: int
first_name: str
hash: str
id: int
photo_url: str
class AuthLoginResponse(CustomModel):
access_token: str

View File

@@ -1,9 +0,0 @@
from pydantic import BaseModel
class AuthLoginRequest(BaseModel):
auth_date: int
first_name: str
hash: str
id: int
photo_url: str

View File

@@ -1,6 +0,0 @@
from pydantic import BaseModel
class AuthLoginResponse(BaseModel):
jwt_token: str
ok: bool

View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel
class CustomModel(BaseModel):
pass

18
schemas/deal.py Normal file
View File

@@ -0,0 +1,18 @@
from schemas.base import CustomModel
class DealChangeStatusRequest(CustomModel):
deal_id: int
new_status: int
class DealChangeStatusResponse(CustomModel):
ok: bool
class DealCreateRequest(CustomModel):
name: str
class DealCreateResponse(CustomModel):
ok: bool

0
services/__init__.py Normal file
View File

56
services/auth.py Normal file
View File

@@ -0,0 +1,56 @@
from typing import Union, Annotated
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status
import backend.config
from backend.session import get_session
from models import User
from services.base import BaseService
from schemas.auth import *
oauth2_schema = HTTPBearer()
algorithm = 'HS256'
async def get_current_user(session: Annotated[AsyncSession, Depends(get_session)],
token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)]) -> User | None:
if not token.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
try:
payload = jwt.decode(token.credentials, backend.config.SECRET_KEY, algorithms=[algorithm])
user_id = payload.get('sub')
if not user_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials')
user_id = int(user_id)
user = await session.get(User, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials')
return user
except JWTError as e:
print(e)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
class AuthService(BaseService):
@staticmethod
def _generate_jwt_token(user: User) -> str:
payload = {
'sub': str(user.id)
}
return jwt.encode(payload, backend.config.SECRET_KEY, algorithm=algorithm)
async def authenticate(self, request: AuthLoginRequest):
user: Union[User, None] = await self.session.scalar(select(User).where(User.telegram_id == request.id))
if not user:
user = User(telegram_id=request.id,
is_admin=False)
self.session.add(user)
await self.session.commit()
access_token = self._generate_jwt_token(user)
return AuthLoginResponse(access_token=access_token)

8
services/base.py Normal file
View File

@@ -0,0 +1,8 @@
from sqlalchemy.ext.asyncio import AsyncSession
class BaseService:
session: AsyncSession
def __init__(self, session: AsyncSession):
self.session = session

42
services/deal.py Normal file
View File

@@ -0,0 +1,42 @@
import datetime
from sqlalchemy import select
from models import User
from models.deal import *
from schemas.deal import *
from services.base import BaseService
class DealService(BaseService):
async def _get_deal_by_id(self, deal_id) -> Deal:
return await self.session.get(Deal, deal_id)
async def create(self, request: DealCreateRequest) -> DealCreateResponse:
deal = Deal(
name=request.name,
created_at=datetime.datetime.now(),
current_status=DealStatus.AWAITING_ACCEPTANCE
)
self.session.add(deal)
await self.session.commit()
return DealCreateResponse(ok=True)
async def change_status(self, request: DealChangeStatusRequest, user: User) -> DealChangeStatusResponse:
# Changing current status
deal = await self._get_deal_by_id(request.deal_id)
from_status = deal.current_status
deal.current_status = request.new_status
# Append status history
status_change = DealStatusHistory(
deal_id=request.deal_id,
user_id=user.id,
changed_at=datetime.datetime.now(),
from_status=from_status,
to_status=request.new_status
)
self.session.add(status_change)
await self.session.commit()
return DealChangeStatusResponse(ok=True)

9
test/ockho.py Normal file
View File

@@ -0,0 +1,9 @@
from typing import Annotated
def get_v():
return 123
ockho: Annotated[int, get_v]
print(ockho)