crappy
This commit is contained in:
33
auth/jwt.py
Normal file
33
auth/jwt.py
Normal 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
|
||||
@@ -1,8 +1,7 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import settings
|
||||
from backend import config
|
||||
|
||||
|
||||
def _generate_hash(telegram_data: dict):
|
||||
|
||||
@@ -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
19
backend/session.py
Normal 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
|
||||
@@ -1 +0,0 @@
|
||||
from .models.basic import *
|
||||
@@ -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)]
|
||||
@@ -1 +0,0 @@
|
||||
from .basic import User
|
||||
20
main.py
20
main.py
@@ -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}"}
|
||||
|
||||
@@ -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
2
models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .auth import *
|
||||
from .deal import *
|
||||
@@ -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
3
models/base.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
BaseModel = declarative_base()
|
||||
41
models/deal.py
Normal file
41
models/deal.py
Normal 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='Новый статус')
|
||||
@@ -1 +1,3 @@
|
||||
from .auth import auth_router
|
||||
from .deal import deal_router
|
||||
|
||||
|
||||
@@ -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
32
routers/deal.py
Normal 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
13
schemas/auth.py
Normal 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
|
||||
@@ -1,9 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AuthLoginRequest(BaseModel):
|
||||
auth_date: int
|
||||
first_name: str
|
||||
hash: str
|
||||
id: int
|
||||
photo_url: str
|
||||
@@ -1,6 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AuthLoginResponse(BaseModel):
|
||||
jwt_token: str
|
||||
ok: bool
|
||||
@@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CustomModel(BaseModel):
|
||||
pass
|
||||
|
||||
18
schemas/deal.py
Normal file
18
schemas/deal.py
Normal 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
0
services/__init__.py
Normal file
56
services/auth.py
Normal file
56
services/auth.py
Normal 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
8
services/base.py
Normal 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
42
services/deal.py
Normal 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
9
test/ockho.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
def get_v():
|
||||
return 123
|
||||
|
||||
|
||||
ockho: Annotated[int, get_v]
|
||||
print(ockho)
|
||||
Reference in New Issue
Block a user