init commit

This commit is contained in:
prod-tech
2024-11-17 02:31:42 +03:00
commit cf933c770c
217 changed files with 19340 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
+4
View File
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
+68
View File
@@ -0,0 +1,68 @@
from typing import Annotated
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi.security import HTTPAuthorizationCredentials
from fastapi.security import HTTPBearer
import jwt
from app.core.config import config
from app.models.user import User
from .utils import decode_jwt
class BearerAuth(HTTPBearer):
def __init__(self, auto_error: bool = True):
super().__init__(auto_error=auto_error)
async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super().__call__(
request
)
if credentials:
if not credentials.scheme == 'Bearer':
raise HTTPException(
status_code=403, detail='Invalid authentication scheme.'
)
if not self.verify_jwt(credentials.credentials):
raise HTTPException(
status_code=403, detail='Invalid token or expired token.'
)
return credentials.credentials
else:
raise HTTPException(
status_code=403, detail='Invalid authorization code.'
)
def verify_jwt(self, token: str) -> bool:
payload = decode_jwt(token)
is_valid = bool(payload)
return is_valid
oauth2_scheme = BearerAuth()
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
try:
payload = jwt.decode(
token, config.JWT_SECRET_KEY, algorithms=['HS256']
)
user = await User.get_or_create_user(
User(
id=payload['user_id'],
username=payload['username'],
events=[],
items=[],
transactions=[],
)
)
return user
except jwt.ExpiredSignatureError:
return {'error': 'Token is expired'}
except jwt.InvalidTokenError:
return {'error': 'Invalid token'}
+7
View File
@@ -0,0 +1,7 @@
import fastapi
credentials_exception = fastapi.HTTPException(
status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
detail='Could not validate credentials',
headers={'WWW-Authenticate': 'Bearer'},
)
+3
View File
@@ -0,0 +1,3 @@
import fastapi
auth_router = fastapi.APIRouter()
+60
View File
@@ -0,0 +1,60 @@
from datetime import timedelta
import fastapi
from app.api.auth.routers import auth_router
import app.core.security.tokens
from app.models.base import BasicResponse
from app.models.telegram import TelegramInputData
from app.models.tokens import Token
from app.models.user import User
@auth_router.post(
'/token',
responses={
fastapi.status.HTTP_401_UNAUTHORIZED: {
'description': 'Unauthorized',
'model': BasicResponse,
},
},
)
async def authenticate(init_data: TelegramInputData) -> Token:
# if not config.DEBUG:
# fields = init_data.model_dump()
# sorted_fields = sorted(fields.items())
# formatted = [f'{key}={value}' for key, value in sorted_fields]
# data_check_string = '\n'.join(formatted)
# secret_key = hmac.new(
# config.TOKEN_TELEGRAM_API.encode(), b'WebAppData', sha256
# ).digest()
# if (
# hmac.new(
# data_check_string.encode(), secret_key, sha256
# ).hexdigest()
# != init_data.hash
# ):
# print(hmac.new(
# data_check_string.encode(), secret_key, sha256
# ).hexdigest())
# print(init_data.hash)
# raise HTTPException(status_code=403, detail='Unauthorized')
user = await User.get_or_create_user(
User(id=init_data.user.id, username=init_data.user.username)
)
return Token(
access_token=app.core.security.tokens.generate_token(
{'user_id': user.id, 'username': user.username},
expires_delta=timedelta(days=7),
),
token_type='bearer',
)
@auth_router.get('/ping')
def ping() -> str:
return 'pong'
+14
View File
@@ -0,0 +1,14 @@
import jwt
from app.core.config import config
def decode_jwt(token: str) -> dict:
try:
decoded_token = jwt.decode(
token, config.JWT_SECRET_KEY, algorithms=['HS256']
)
return decoded_token
except (jwt.InvalidTokenError, jwt.ExpiredSignatureError):
return {}
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
@@ -0,0 +1,3 @@
import fastapi
calculate_debits_router = fastapi.APIRouter()
+106
View File
@@ -0,0 +1,106 @@
from uuid import UUID
from fastapi import Depends
from fastapi import HTTPException
import pydantic
import requests
from sqlmodel import select
from sqlmodel import Session
from app.api.auth.deps import BearerAuth
from app.api.auth.deps import get_current_user
from app.core.db import engine
from app.models.event import Event
from app.models.user import User
from app.utils.telegram import send_telegram_message
from .routers import calculate_debits_router
class Debt(pydantic.BaseModel):
from_user_id: int
to_user_id: int
amount: float
@calculate_debits_router.post(
'/events/{event_id}',
description='Get debts smeta',
dependencies=[Depends(BearerAuth())],
)
def calculate_event_debts(
event_id: UUID, user: User = Depends(get_current_user)
) -> list[Debt]:
with Session(engine) as session:
result = session.execute(select(Event).where(Event.id == event_id))
event = result.scalars().first()
if not event:
raise HTTPException(status_code=404, detail='Event not found')
if user.id != event.owner_id:
raise HTTPException(status_code=403, detail='Permission denied')
transactions = []
for transaction in event.transactions:
if not transaction.closed:
positions = [
{
'price': item.price,
# 'assigned_to_ids': [
# user.id for user in item.assigned_to
# ],
'assigned_to_ids': [user.id for user in event.users],
}
for item in transaction.items
]
transactions.append(
{'owner_id': transaction.payer_id, 'positions': positions}
)
transaction.closed = True
if not transactions:
return []
participants = [{'id': user.id} for user in event.users]
input_data = {
'participants': participants,
'transactions': transactions,
}
response = requests.post(
'http://optimizetka:8000/optimizetka/api/calculate-debts',
json=input_data,
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f'Failed to calculate debts: {response.text}',
)
debts = response.json()
session.commit()
messages = {user.id: [] for user in event.users}
print(debts)
for debt in debts:
from_user = next(
u for u in event.users if u.id == debt['from_user_id']
)
to_user = next(
u for u in event.users if u.id == debt['to_user_id']
)
print(from_user, to_user)
amount = debt['amount']
messages[from_user].append(f'You owe {amount} to {to_user.name}.')
messages[to_user.id].append(f'{from_user.name} owes you {amount}.')
for user in event.users:
message_text = '\n'.join(messages[user.id])
if message_text:
send_telegram_message(user.chat_id, message_text)
return debts
+5
View File
@@ -0,0 +1,5 @@
__all__: list[str] = []
# Initialize all routes
from .invites import *
from .routes import *
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
@@ -0,0 +1,3 @@
import fastapi
invites_router = fastapi.APIRouter()
+44
View File
@@ -0,0 +1,44 @@
import typing
import uuid
import fastapi
from fastapi import HTTPException
import sqlmodel
import app.api.auth.deps
from app.api.auth.deps import BearerAuth
from app.api.events.invites.routers import invites_router
import app.core.db
from app.models import Event
import app.models.base
from app.models.user import User
@invites_router.get(
'/{invite_id}',
responses={
fastapi.status.HTTP_400_BAD_REQUEST: {
'model': app.models.base.BasicResponse,
'detail': 'Invalid invite',
}
},
dependencies=[fastapi.Depends(BearerAuth())],
)
def join_by_invite(
invite_id: uuid.UUID,
user: typing.Annotated[
User, fastapi.Depends(app.api.auth.deps.get_current_user)
],
) -> app.models.event.Event:
with sqlmodel.Session(app.core.db.engine) as session:
user = session.get(User, user.id)
event = session.query(Event).filter_by(invite=invite_id).first()
if not event:
raise HTTPException(
fastapi.status.HTTP_404_NOT_FOUND, detail='Invite not found'
)
event.users.append(user)
session.commit()
return event
+9
View File
@@ -0,0 +1,9 @@
import fastapi
# from .invites.routers import invites_router
events_router = fastapi.APIRouter()
# events_router.include_router(
# invites_router, prefix='/invites', tags=['invites']
# )
+151
View File
@@ -0,0 +1,151 @@
import typing
import uuid
import fastapi
import sqlmodel
import app.api.auth.deps
from app.api.auth.deps import BearerAuth
from app.api.events.routers import events_router
import app.core.db
from app.models import Event
from app.models import OutputEvent
import app.models.event
from app.models.user import User
@events_router.get(
'/',
response_model=list[app.models.event.OutputEvent],
description='Return events containing given user (by token)',
dependencies=[fastapi.Depends(BearerAuth())],
)
def list_events(
user: typing.Annotated[
User, fastapi.Depends(app.api.auth.deps.get_current_user)
],
):
output = []
with sqlmodel.Session(app.core.db.engine) as session:
user = session.get(User, user.id)
for event in user.events:
new_output = app.models.event.OutputEvent(
id=event.id,
owner=event.owner_id,
users=event.users,
invite=event.invite,
name=event.name,
)
output.append(new_output)
return output
@events_router.post(
'/add/{event_id}',
response_model=Event,
description='Add user to event',
dependencies=[fastapi.Depends(BearerAuth())],
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': r'Event \ user not found',
},
},
)
async def add_to_event(
user: typing.Annotated[
User, fastapi.Depends(app.api.auth.deps.get_current_user)
],
event_id: uuid.UUID,
):
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(Event, event_id)
if not event:
raise fastapi.HTTPException(
detail='Event not found', status_code=404
)
user = session.get(User, user.id)
if not user:
raise fastapi.HTTPException(
detail='User not found', status_code=404
)
event.users.append(user)
session.add(event)
session.commit()
session.refresh(event)
return event
@events_router.post(
'/',
description='Create event',
dependencies=[fastapi.Depends(BearerAuth())],
)
def create_event(
event: app.models.event.BaseEvent,
user: typing.Annotated[
User, fastapi.Depends(app.api.auth.deps.get_current_user)
],
) -> app.models.event.Event:
with sqlmodel.Session(app.core.db.engine) as session:
new_event = app.models.event.Event(
name=event.name, owner_id=user.id, users=[user]
)
session.add(new_event)
session.commit()
session.refresh(new_event)
return new_event
# @events_router.delete(
# '/{event_id}',
# dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
# description='Delete an event',
# )
# def delete_event(
# event_id: uuid.UUID,
# ):
# with sqlmodel.Session(app.core.db.engine) as session:
# session.delete(session.get(app.models.event.Event, event_id))
# session.commit()
@events_router.get(
'/{event_id}',
response_model=app.models.event.OutputEvent,
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
description='Get info for an event by the id',
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': r'Event \ user not found',
},
},
)
def event_by_id(event_id: uuid.UUID):
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(app.models.event.Event, event_id)
if not event:
raise fastapi.HTTPException(
detail='Event not found', status_code=404
)
new_event = app.models.event.OutputEvent(
id=event.id,
owner=event.owner_id,
users=event.users,
invite=event.invite,
name=event.name,
)
return new_event
+4
View File
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
from fastapi import Depends
from app.api.auth.deps import BearerAuth
items_router = APIRouter(
dependencies=[Depends(BearerAuth())],
)
+123
View File
@@ -0,0 +1,123 @@
from uuid import UUID
import fastapi
import sqlmodel
import app.core.db
from app.models.event import Event
from app.models.item import Item
from app.models.item import ItemRequest
from app.models.item import OutputItem
from app.models.transactions import Transaction
from ...models import BasicResponse
from ..auth.deps import BearerAuth
from .routers import items_router
@items_router.get(
'/{transaction_id}',
description='Get items from transaction',
responses={
404: {
'model': BasicResponse,
'description': 'Transaction by this ID is not found',
},
403: {'model': BasicResponse, 'description': 'Unauthorized'},
},
)
async def get_items(transaction_id: UUID) -> list[OutputItem]:
with sqlmodel.Session(bind=app.core.db.engine) as session:
transaction = session.get(Transaction, transaction_id)
if not transaction:
raise fastapi.HTTPException(
detail='Transaction not found', status_code=404
)
out = []
for ti in transaction.items:
out.append(OutputItem(
title=ti.title,
id=ti.id,
price=ti.price,
assigned_to=ti.assigned_to,
transaction_id=ti.transaction_id
))
return out
@items_router.post(
'/{transaction_id}',
description='Create item in transaction',
responses={
400: {
'model': BasicResponse,
'description': 'Invalid data format (ex. negative price)',
},
404: {
'model': BasicResponse,
'description': 'Transaction with this ID is not found',
},
403: {'model': BasicResponse, 'description': 'Unauthorized'},
},
)
async def create_item(item: ItemRequest, transaction_id: UUID) -> OutputItem:
if item.price <= 0:
raise fastapi.HTTPException(
detail='Price cannot be null or negative', status_code=400
)
with sqlmodel.Session(bind=app.core.db.engine) as session:
transaction = session.get(Transaction, transaction_id)
if not transaction:
raise fastapi.HTTPException(
detail='Transaction not found', status_code=404
)
event = session.get(Event, transaction.event_id)
new_item = Item(
title=item.title,
price=item.price,
assigned_to=[] if not item.all_users_selected else event.users,
transaction_id=transaction.id,
)
session.add(new_item)
session.commit()
session.refresh(new_item)
return OutputItem(
title=item.title,
id=item.id,
price=item.price,
assigned_to=item.assigned_to,
transaction_id=item.transaction_id
)
@items_router.get(
path='/{transaction_id}/{item_id}',
description='Get item by itself ID',
dependencies=[fastapi.Depends(BearerAuth())],
responses={
404: {
'model': BasicResponse,
'description': 'Item with this ID is not found',
},
403: {'model': BasicResponse, 'description': 'Unauthorized'},
},
)
async def get_item(transaction_id: UUID, item_id: UUID) -> OutputItem:
with sqlmodel.Session(bind=app.core.db.engine) as session:
item = session.get(Item, item_id)
if not item:
raise fastapi.HTTPException(
detail='Item not found', status_code=404
)
return OutputItem(
id=item.id,
title=item.title,
price=item.price,
assigned_to=item.assigned_to,
transaction_id=transaction_id,
)
+33
View File
@@ -0,0 +1,33 @@
import fastapi
import app.api.auth.routers
import app.api.calculate_debits.routers
import app.api.events.routers
import app.api.items.routers
import app.api.transactions.routers
import app.api.utils.routers
api_router = fastapi.APIRouter()
api_router.include_router(
app.api.auth.routers.auth_router, prefix='/auth', tags=['auth']
)
api_router.include_router(
app.api.utils.routers.utils_router, prefix='/utils', tags=['utils']
)
api_router.include_router(
app.api.events.routers.events_router, prefix='/events', tags=['events']
)
api_router.include_router(
app.api.transactions.routers.transactions_router,
prefix='/transaction',
tags=['transactions'],
)
api_router.include_router(
app.api.calculate_debits.routers.calculate_debits_router,
prefix='/calculate_debits',
tags=['calculate_debits'],
)
api_router.include_router(
app.api.items.routers.items_router, prefix='/items', tags=['items']
)
View File
+4
View File
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
+3
View File
@@ -0,0 +1,3 @@
from fastapi import APIRouter
transactions_router = APIRouter()
+208
View File
@@ -0,0 +1,208 @@
import typing
import uuid
import fastapi
import sqlmodel
import app.api.auth.deps
from app.api.transactions.routers import transactions_router
import app.core.db
import app.models.item
@transactions_router.get(
'/{event_id}/{transaction_id}',
response_model=app.models.transactions.OutputTransaction,
description='Get a transaction by its event and transaction id',
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': r'Event \ transaction not found',
},
},
)
async def get_transaction(
event_id: uuid.UUID,
transaction_id: uuid.UUID,
):
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(app.models.event.Event, event_id)
if not event:
raise fastapi.HTTPException(
status_code=404, detail='Event not found'
)
transaction = session.get(
app.models.transactions.Transaction, transaction_id
)
output_transaction = app.models.transactions.OutputTransaction(
title=transaction.title,
payer=transaction.payer,
event_id=transaction.event_id,
closed=transaction.closed,
items=transaction.items,
id=transaction.id,
)
return output_transaction
@transactions_router.get(
'/{event_id}',
description='Get all transactions of an event',
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': r'Event not found',
},
},
)
async def list_transactions(
event_id: uuid.UUID,
) -> list[app.models.transactions.OutputTransaction]:
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(app.models.event.Event, event_id)
if not event:
raise fastapi.HTTPException(
status_code=404, detail='Event not found'
)
output = []
for transaction in event.transactions:
output.append(
app.models.transactions.OutputTransaction(
title=transaction.title,
payer=transaction.payer,
event_id=transaction.event_id,
closed=transaction.closed,
items=transaction.items,
id=transaction.id,
)
)
return output
@transactions_router.post(
'/{event_id}',
response_model=app.models.transactions.OutputTransaction,
description='Create a transaction by a event id',
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': 'Event not found',
},
},
)
async def create_transaction(
title: typing.Annotated[
str, fastapi.Body(description='Title of transaction')
],
event_id: uuid.UUID,
user: typing.Annotated[
app.models.user.User,
fastapi.Depends(app.api.auth.deps.get_current_user),
],
):
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(app.models.event.Event, event_id)
if not event:
raise fastapi.HTTPException(
status_code=404, detail='Event not found'
)
transaction = app.models.transactions.Transaction(
title=title,
payer_id=user.id,
event_id=event.id,
items=[],
)
session.add(transaction)
session.commit()
session.refresh(transaction)
output_transaction = app.models.transactions.OutputTransaction(
id=transaction.id,
title=transaction.title,
payer=transaction.payer,
event_id=transaction.event_id,
closed=transaction.closed,
items=transaction.items,
)
return output_transaction
@transactions_router.post(
'/{event_id}/{transaction_id}/items',
response_model=app.models.item.Item,
description='Create a transaction by a event id',
dependencies=[fastapi.Depends(app.api.auth.deps.get_current_user)],
responses={
fastapi.status.HTTP_404_NOT_FOUND: {
'model': app.models.base.BasicResponse,
'detail': r'Event \ transaction not found',
},
400: {
'model': app.models.base.BasicResponse,
'description': 'Invalid data format (ex. negative price)',
},
},
)
async def add_item_to_transaction(
event_id: uuid.UUID,
transaction_id: uuid.UUID,
title: str = fastapi.Body(description='Title of item'),
price: float = fastapi.Body(description='Price of item'),
add_all_users: bool = fastapi.Body(
description='Assign all users to this item', default=False
),
):
with sqlmodel.Session(app.core.db.engine) as session:
event = session.get(app.models.event.Event, event_id)
if not event:
raise fastapi.HTTPException(
status_code=404, detail='Event not found'
)
transaction = session.get(
app.models.transactions.Transaction, transaction_id
)
if not transaction:
raise fastapi.HTTPException(
status_code=404, detail='Transaction not found'
)
if transaction.event_id != event.id:
raise fastapi.HTTPException(
status_code=404,
detail='This transaction is not a child of this event',
)
item = app.models.item.Item(
title=title,
price=price,
assigned_to=[] if not add_all_users else event.users,
transaction_id=transaction_id,
)
if item.price <= 0:
raise fastapi.HTTPException(
detail='Price cannot be null or negative', status_code=400
)
session.add(item)
session.commit()
session.refresh(item)
return item
+4
View File
@@ -0,0 +1,4 @@
__all__: list[str] = []
# Initialize all routes
from .routes import *
+3
View File
@@ -0,0 +1,3 @@
import fastapi
utils_router = fastapi.APIRouter()
+78
View File
@@ -0,0 +1,78 @@
import fastapi
from fastapi import HTTPException
import sqlmodel
from app.api.auth.deps import BearerAuth
from app.api.auth.deps import get_current_user
from app.api.utils.routers import utils_router
import app.core.db
from app.models import BasicResponse
from app.models import User
from app.models.base import OfdRequest
from app.models.event import Event
from app.models.item import Item
from app.models.transactions import Transaction
from app.utils.nalog import get_nalog_data
@utils_router.get('/health-check')
def health_check() -> dict[str, str]:
return {'msg': 'healthy'}
@utils_router.post(
'/ofd',
description='Get items info from OFD bare string',
dependencies=[fastapi.Depends(BearerAuth())],
responses={
400: {'model': BasicResponse, 'description': 'Bad OFD data'},
403: {'model': BasicResponse, 'description': 'Unauthorized'},
404: {'model': BasicResponse, 'description': 'Event not found'},
500: {
'model': BasicResponse,
'description': 'Error while processing OFD data',
},
},
)
async def ofd(
ofd: OfdRequest, user: User = fastapi.Depends(get_current_user)
) -> Transaction:
with sqlmodel.Session(bind=app.core.db.engine) as session:
event = session.get(Event, ofd.event_id)
if not event:
raise HTTPException(status_code=404, detail='Event not found')
data = await get_nalog_data(ofd.ofd_string)
data = data['data']['json']
if not data:
raise HTTPException(status_code=400, detail='Bad OFD data')
transaction = Transaction(
payer_id=user.id, event_id=event.id, title=data['retailPlace']
)
session.add(transaction)
session.commit()
items = data['items']
for item in items:
new_item = Item(
title=item['name'],
price=item['sum'] / 100,
transaction_id=transaction.id,
)
session.add(new_item)
session.commit()
transaction.items.append(new_item)
session.add(transaction)
session.refresh(transaction)
session.commit()
try:
return transaction
except Exception:
raise HTTPException(
detail='Error while getting information about check',
status_code=500,
)