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 hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
|
|
||||||
import settings
|
from backend import config
|
||||||
|
|
||||||
|
|
||||||
def _generate_hash(telegram_data: dict):
|
def _generate_hash(telegram_data: dict):
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ PG_HOST = os.environ.get('PG_HOST')
|
|||||||
|
|
||||||
# Telegram
|
# Telegram
|
||||||
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN')
|
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
|
import routers
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
routers_list = [
|
routers_list = [
|
||||||
routers.auth_router
|
routers.auth_router,
|
||||||
|
routers.deal_router
|
||||||
]
|
]
|
||||||
for router in routers_list:
|
for router in routers_list:
|
||||||
app.include_router(router)
|
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 asyncio
|
||||||
|
import backend.config as settings
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from sqlalchemy import pool
|
|
||||||
from sqlalchemy.engine import Connection
|
from sqlalchemy.engine import Connection
|
||||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
import settings
|
from backend.session import engine
|
||||||
from database.base import engine, BaseModel
|
from models import BaseModel
|
||||||
from database.models import *
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# 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):
|
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 .auth import auth_router
|
||||||
|
from .deal import deal_router
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from fastapi import APIRouter
|
from typing import Annotated
|
||||||
from sqlalchemy import select, insert
|
|
||||||
|
|
||||||
import database
|
from fastapi import APIRouter, Depends
|
||||||
from database import User
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from database.base import DatabaseDependency
|
|
||||||
from schemas.auth.requests import *
|
from backend.session import get_session
|
||||||
from auth.telegram import telegram_authorize
|
from schemas.auth import AuthLoginRequest, AuthLoginResponse
|
||||||
from schemas.auth.responses import AuthLoginResponse
|
from services.auth import AuthService
|
||||||
|
|
||||||
auth_router = APIRouter(
|
auth_router = APIRouter(
|
||||||
prefix='/auth',
|
prefix='/auth',
|
||||||
@@ -15,6 +14,5 @@ auth_router = APIRouter(
|
|||||||
|
|
||||||
|
|
||||||
@auth_router.post('/login', response_model=AuthLoginResponse)
|
@auth_router.post('/login', response_model=AuthLoginResponse)
|
||||||
async def login(request: AuthLoginRequest, db_session: DatabaseDependency):
|
async def login(request: AuthLoginRequest, session: Annotated[AsyncSession, Depends(get_session)]):
|
||||||
existing_user: User = await db_session.scalar(select(User).where(User.telegram_id == request.id))
|
return await AuthService(session).authenticate(request)
|
||||||
return AuthLoginResponse(ok=True, jwt_token="dasdasd")
|
|
||||||
|
|||||||
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