26 Commits

Author SHA1 Message Date
6c6f96cc94 Trying to change models 2023-07-24 07:07:36 +03:00
45d0b8e52d Alembic installed and activated. Poems table added 2023-07-23 22:52:31 +03:00
1055416640 Still no result 2023-07-20 00:09:30 +03:00
52d9ad3399 Prepare to use another auth code 2023-07-19 23:24:42 +03:00
30140f058f Добавлена response_model=User в get_current_user 2023-07-19 00:14:56 +03:00
d5ba710885 добавили responce_model=User к get_current_user 2023-07-19 00:11:57 +03:00
6127dd8ba4 добавлен параметр response_model к get_user 2023-07-19 00:10:59 +03:00
9e4bb1b99f К схемам из schema.py добавлены доп. поля (соотв. models) 2023-07-19 00:05:42 +03:00
d66b9004e0 fastapi.Responce has been imported 2023-07-18 23:56:07 +03:00
626170964f pass new parameters to sessionmaker 2023-07-18 23:47:01 +03:00
1dd37a72b4 parameters of sessionmaker changed 2023-07-18 23:43:23 +03:00
b39d9ada27 Imported modules corrected 2023-07-18 23:39:04 +03:00
91842dcc51 Merge branch 'main' of https://git.dm1sh.ru/dm1sh/porridger 2023-07-17 23:36:46 +03:00
09ba6a3478 Разделил модели базы данных и модели pydantic. 2023-07-17 23:34:04 +03:00
de8a1abcbf Fixed Dima's incomplete merge 2023-07-17 15:19:26 +03:00
898d590cf3 Обновить back/main.py 2023-07-17 13:23:23 +03:00
1c25d66027 Fixed trashbox request causing constant rerenders 2023-07-17 12:19:25 +03:00
b7bbd937b4 Converted put(api/announcement) to use useSend
Added useSendButtonCaption hook
Related to #19
2023-07-17 12:18:54 +03:00
808edad6b4 Необходимо решить проблему get_user 2023-07-17 03:41:20 +03:00
b360b06d34 Возвращаем просто токен, а не джисонку 2023-07-16 23:25:22 +03:00
349d40daa4 Merge branch 'main' of https://git.dm1sh.ru/dm1sh/porridger 2023-07-16 16:15:05 +03:00
8bae63231b Добавить в остальные функции проверку токена 2023-07-16 16:10:37 +03:00
3668e8c33f Changed story dimensions
Improved logging
2023-07-16 00:18:30 +03:00
f4226237ab Added useSend hook, converted useFetch to use it
Moved useFetch
Related to #19
2023-07-16 00:08:51 +03:00
5b14d75048 Обновить back/main.py 2023-07-15 13:17:07 +03:00
0218cdced5 Code styling
Added brackets for const lambdas
Converted const lambdas with multiple instructions to functions
2023-07-15 12:55:25 +03:00
48 changed files with 1062 additions and 623 deletions

102
alembic.ini Normal file
View File

@ -0,0 +1,102 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

2
back/base.py Normal file
View File

@ -0,0 +1,2 @@
from .db import Base
from .models import UserDatabase, Announcement, Trashbox

View File

@ -1,6 +1,13 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from typing import AsyncGenerator
from sqlalchemy import create_engine, select
# from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.ext.declarative import declarative_base
from fastapi import Depends
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
@ -8,6 +15,7 @@ engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autoflush=True, bind=engine)
SessionLocal = sessionmaker(bind=engine, autoflush=True, autocommit=False)
database = SessionLocal()
Base = declarative_base()

View File

@ -19,34 +19,47 @@ import shutil
import os
from .utils import *
from .models import Announcement, Trashbox, UserDatabase, Base
from .db import engine, SessionLocal
from .db import Base, engine, SessionLocal, database
from .models import Announcement, Trashbox, UserDatabase
from . import schema
Base.metadata.create_all(bind=engine)
db = SessionLocal()
app = FastAPI()
templates = Jinja2Templates(directory="./front/dist")
app.mount("/static", StaticFiles(directory = "./front/dist"))
if not os.path.exists("./uploads"):
os.mkdir("C:/Users/38812/porridger/uploads")
app.mount("/uploads", StaticFiles(directory = "./uploads"))
# Функция, создающая сессию БД при каждом запросе к нашему API.
# Срабатывает до запуска остальных функций.
# Всегда закрывает сессию при окончании работы с ней
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
@app.get("/api/announcements")#адрес объявлений
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = -1):
def annoncements_list(user_id: int = None, metro: str = None, category: str = None, booked_by: int = 0):
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
a = db.query(Announcement)
b = db.query(Announcement)
c = db.query(Announcement)
d = db.query(Announcement)
e = db.query(Announcement)
a = database.query(Announcement)
b = database.query(Announcement)
c = database.query(Announcement)
d = database.query(Announcement)
e = database.query(Announcement)
if user_id != None:
b = a.filter(Announcement.user_id == user_id)
@ -74,7 +87,7 @@ def single_annoncement(user_id:int):
# Считываем данные из Body и отображаем их на странице.
# В последствии будем вставлять данные в html-форму
try:
annoncement = db.get(Announcement, user_id)
annoncement = database.get(Announcement, user_id)
return {"id": annoncement.id, "user_id": annoncement.user_id, "name": annoncement.name,
"category": annoncement.category, "best_by": annoncement.best_by, "address": annoncement.address,
"description": annoncement.description, "metro": annoncement.metro, "latitude": annoncement.latitude,
@ -86,7 +99,7 @@ def single_annoncement(user_id:int):
# Занести объявление в базу
@app.put("/api/announcement")#адрес объявлений
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: Annotated[UploadFile | None, File()], metro: Annotated[str, Form()], trashId: Annotated[int | None, Form()] = -1):
def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], bestBy: Annotated[int, Form()], address: Annotated[str, Form()], longtitude: Annotated[float, Form()], latitude: Annotated[float, Form()], description: Annotated[str, Form()], src: UploadFile, metro: Annotated[str, Form()], trashId: Annotated[int, Form()] = None):
# try:
userId = 1 # temporary
@ -102,7 +115,7 @@ def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], be
uploaded_name = "/uploads/"+destination.name
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, address=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId)
temp_ancmt = Announcement(user_id=userId, name=name, category=category, best_by=bestBy, address=address, longtitude=longtitude, latitude=latitude, description=description, src=uploaded_name, metro=metro, trashId=trashId, booked_by=-1)
db.add(temp_ancmt) # добавляем в бд
db.commit() # сохраняем изменения
db.refresh(temp_ancmt) # обновляем состояние объекта
@ -115,8 +128,8 @@ def put_in_db(name: Annotated[str, Form()], category: Annotated[str, Form()], be
@app.delete("/api/announcement") #адрес объявления
def delete_from_db(data = Body()):#функция удаления объекта из БД
try:
db.delete(user_id=data.user_id)#удаление из БД
db.commit() # сохраняем изменения
database.delete(user_id=data.user_id)#удаление из БД
database.commit() # сохраняем изменения
return {"Answer" : True}
except:
return {"Answer" : False}
@ -129,21 +142,21 @@ def change_book_status(data: schema.Book):
# Получаем id пользователя, который бронирует объявление
temp_user_id = 1
# Находим объявление по данному id
announcement_to_change = db.query(Announcement).filter(id == data.id).first()
announcement_to_change = database.query(Announcement).filter(id == data.id).first()
# Изменяем поле booked_status на полученный id
announcement_to_change.booked_status = temp_user_id
return {"Success": True}
except:
return {"Success": False}
# reginstration
@app.post("/api/signup")
def create_user(data = Body()):
if db.query(UserDatabase).filter(User.email == data["email"]).first() == None:
if database.query(UserDatabase).filter(UserDatabase.email == data["email"]).first() == None:
new_user = UserDatabase(id=data["id"], email=data["email"], password=data["password"], name=data["name"], surname=data["surname"])
db.add(new_user)
db.commit()
db.refresh(new_user) # обновляем состояние объекта
database.add(new_user)
database.commit()
database.refresh(new_user) # обновляем состояние объекта
return {"Success": True}
return {"Success": False, "Message": "Пользователь с таким email уже зарегестрирован."}
@ -152,7 +165,8 @@ def create_user(data = Body()):
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
):
user = authenticate_user(db.query(UserDatabase).all(), form_data.username, form_data.password)
# разобраться с первым параметром
user = authenticate_user(database, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -163,11 +177,11 @@ async def login_for_access_token(
access_token = create_access_token(
data={"user_id": user.id}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
return access_token
@app.get("/api/users/me/", response_model=User)
async def read_users_me(
@app.get("/api/users/me/", response_model=schema.User)
async def read_users_me( #!!!!!!!!!!!
current_user: Annotated[User, Depends(get_current_active_user)]
):
return current_user
@ -184,7 +198,7 @@ async def read_own_items(
@app.get("/api/trashbox")
def get_trashboxes(lat:float, lng:float):#крутая функция для работы с api
BASE_URL='https://geointelect2.gate.petersburg.ru'#адрес сайта и мой токин
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3Nzg2NTk4MjEsImlhdCI6MTY4Mzk2NTQyMSwianRpIjoiOTI2ZGMyNmEtMGYyZi00OTZiLWI0NTUtMWQyYWM5YmRlMTZkIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjE2MGU1ZGVkLWFmMjMtNDkyNS05OTc1LTRhMzM0ZjVmNTkyOSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiIxNjBlNWRlZC1hZjIzLTQ5MjUtOTk3NS00YTMzNGY1ZjU5MjkiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.BRyUIyY-KKnZ9xqTNa9vIsfKF0UN2VoA9h4NN4y7IgBVLiiS-j43QbeE6qgjIQo0pV3J8jtCAIPvJbO-Ex-GNkw_flgMiGHhKEpsHPW3WK-YZ-XsZJzVQ_pOmLte-Kql4z97WJvolqiXT0nMo2dlX2BGvNs6JNbupvcuGwL4YYpekYAaFNYMQrxi8bSN-R7FIqxP-gzZDAuQSWRRSUqVBLvmgRhphTM-FAx1sX833oXL9tR7ze3eDR_obSV0y6cKVIr4eIlKxFd82qiMrN6A6CTUFDeFjeAGERqeBPnJVXU36MHu7Ut7eOVav9OUARARWRkrZRkqzTfZ1iqEBq5Tsg'
my_token='eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJhU1RaZm42bHpTdURYcUttRkg1SzN5UDFhT0FxUkhTNm9OendMUExaTXhFIn0.eyJleHAiOjE3ODM3ODk4NjgsImlhdCI6MTY4OTA5NTQ2OCwianRpIjoiNDUzNjQzZTgtYTkyMi00NTI4LWIzYmMtYWJiYTNmYjkyNTkxIiwiaXNzIjoiaHR0cHM6Ly9rYy5wZXRlcnNidXJnLnJ1L3JlYWxtcy9lZ3MtYXBpIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImJjYjQ2NzljLTU3ZGItNDU5ZC1iNWUxLWRlOGI4Yzg5MTMwMyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLXJlc3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImM2ZDJiOTZhLWMxNjMtNDAxZS05ZjMzLTI0MmE0NDcxMDY5OCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiLyoiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtZWdzLWFwaSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJjNmQyYjk2YS1jMTYzLTQwMWUtOWYzMy0yNDJhNDQ3MTA2OTgiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiLQktC70LDQtNC40LzQuNGAINCv0LrQvtCy0LvQtdCyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZTBmYzc2OGRhOTA4MjNiODgwZGQzOGVhMDJjMmQ5NTciLCJnaXZlbl9uYW1lIjoi0JLQu9Cw0LTQuNC80LjRgCIsImZhbWlseV9uYW1lIjoi0K_QutC-0LLQu9C10LIifQ.E2bW0B-c6W5Lj63eP_G8eI453NlDMnW05l11TZT0GSsAtGayXGaolHtWrmI90D5Yxz7v9FGkkCmcUZYy1ywAdO9dDt_XrtFEJWFpG-3csavuMjXmqfQQ9SmPwDw-3toO64NuZVv6qVqoUlPPj57sLx4bLtVbB4pdqgyJYcrDHg7sgwz4d1Z3tAeUfSpum9s5ZfELequfpLoZMXn6CaYZhePaoK-CxeU3KPBPTPOVPKZZ19s7QY10VdkxLULknqf9opdvLs4j8NMimtwoIiHNBFlgQz10Cr7bhDKWugfvSRsICouniIiBJo76wrj5T92s-ztf1FShJuqnQcKE_QLd2A'
head = {'Authorization': 'Bearer {}'.format(my_token)}
my_data={

View File

@ -1,12 +1,18 @@
from sqlalchemy import Column, Integer, String
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from fastapi import Depends
from .db import Base
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
class UserDatabase(Base):#класс пользователя
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)#айди пользователя
id = Column(Integer, primary_key=True, index=True, unique=True)#айди пользователя
phone = Column(Integer, nullable=True)#номер телефона пользователя
email = Column(String)#электронная почта пользователя
password = Column(String) # пароль
@ -43,3 +49,26 @@ class Trashbox(Base):#класс мусорных баков
longtitude = Column(Integer)
category = Column(String)#категория продукта из объявления
class Poems(Base):#класс поэзии
__tablename__ = "poems"
id = Column(Integer, primary_key=True, index=True) #айди
poem_text = Column(String) # текст стихотворения
# from typing import AsyncGenerator
# from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
# # This function can be called during the initialization of the FastAPI app.
# async def create_db_and_tables():
# async with engine.begin() as conn:
# await conn.run_sync(Base.metadata.create_all)
# async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
# async with async_session_maker() as session:
# yield session
# async def get_user_db(session: AsyncSession = Depends(get_async_session)):
# yield SQLAlchemyUserDatabase(session, User)

View File

@ -1,5 +1,29 @@
from pydantic import BaseModel
from typing import Annotated, Union
class Book(BaseModel):
id: int
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Union[str, None] = None
class User(BaseModel):
id: int
phone: Union[int, None] = None
email: str
name: Union[str, None] = None
surname: str
class Config:
orm_mode = True
class UserInDB(User):
password: str
hashed_password: str

2
back/service.py Normal file
View File

@ -0,0 +1,2 @@
from sqlalchemy.orm import Session

View File

@ -1,96 +1,24 @@
# from passlib.context import CryptContext
# import os
# from datetime import datetime, timedelta
# from typing import Union, Any
# from jose import jwt
# ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 30 minutes
# REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days
# ALGORITHM = "HS256"
# # В предположении, что попыток взлома не будет, возьмем простейший ключ
# JWT_SECRET_KEY = "secret key" # может также быть os.environ["JWT_SECRET_KEY"]
# JWT_REFRESH_SECRET_KEY = "refresh secret key" # может также быть os.environ["JWT_REFRESH_SECRET_KEY"]
# def get_hashed_password(password: str) -> str:
# return password_context.hash(password)
# def verify_password(password: str, hashed_pass: str) -> bool:
# return password_context.verify(password, hashed_pass)
# def create_access_token(subject: Union[str, Any], expires_delta: int = None) -> str:
# if expires_delta is not None:
# expires_delta = datetime.utcnow() + expires_delta
# else:
# expires_delta = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# to_encode = {"exp": expires_delta, "sub": str(subject)}
# encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, ALGORITHM)
# return encoded_jwt
# def create_refresh_token(subject: Union[str, Any], expires_delta: int = None) -> str:
# if expires_delta is not None:
# expires_delta = datetime.utcnow() + expires_delta
# else:
# expires_delta = datetime.utcnow() + timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
# to_encode = {"exp": expires_delta, "sub": str(subject)}
# encoded_jwt = jwt.encode(to_encode, JWT_REFRESH_SECRET_KEY, ALGORITHM)
# return encoded_jwt
from datetime import datetime, timedelta
from typing import Annotated, Union
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi import Depends, FastAPI, HTTPException, status, Response, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
from sqlalchemy.orm import Session
from sqlalchemy import select
from .db import Session, database
from .models import UserDatabase
from .schema import Token, TokenData, UserInDB, User
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# fake_users_db = {
# "johndoe": {
# "email": "johndoe",
# "full_name": "John Doe",
# "email": "johndoe@example.com",
# "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
# "disabled": False,
# }
# }
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: Union[str, None] = None
class User(BaseModel):
email: str
email: Union[str, None] = None
# password: str
# password: Union[str, None] = None
full_name: Union[str, None] = None
disabled: Union[bool, None] = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@ -103,17 +31,16 @@ def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, email: str):
user = None
for person_with_correct_email in db:
if person_with_correct_email.email == email:
user = person_with_correct_email
break
return user #UserInDB(user_email)
# проблема здесь
def get_user(db: Session, email: str):
user_with_required_email = db.query(UserDatabase).filter(UserDatabase.email == email).first()
print(user_with_required_email)
if user_with_required_email:
return user_with_required_email
return None
def authenticate_user(db, email: str, password: str):
def authenticate_user(db: Session, email: str, password: str):
user = get_user(db, email)
if not user:
return False
@ -133,7 +60,7 @@ def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
async def get_current_user(db: Session, token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
@ -147,8 +74,8 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, email=token_data.email)
if user is None:
user = get_user(db, email=token_data.email)
if user == None:
raise credentials_exception
return user
@ -159,3 +86,7 @@ async def get_current_active_user(
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_db(request: Request):
return request.state.db

View File

@ -17,7 +17,8 @@ type AnnouncementResponse = {
booked_by: number
}
const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => isObject(obj, {
const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => (
isObject(obj, {
'id': 'number',
'user_id': 'number',
'name': 'string',
@ -31,7 +32,8 @@ const isAnnouncementResponse = (obj: unknown): obj is AnnouncementResponse => is
'metro': 'string',
'trashId': 'number?',
'booked_by': 'number'
})
})
)
type Announcement = {
id: number,

View File

@ -5,8 +5,9 @@ import { AnnouncementsResponse } from './types'
const initialAnnouncements: Announcement[] = []
const composeAnnouncementsURL = (filters: FiltersType) =>
const composeAnnouncementsURL = (filters: FiltersType) => (
API_URL + '/announcements?' + new URLSearchParams(URLEncodeFilters(filters)).toString()
)
const processAnnouncements = (data: AnnouncementsResponse): Announcement[] => {
const annList = data.list_of_announcements

View File

@ -6,10 +6,12 @@ type AnnouncementsResponse = {
Success: boolean
}
const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => isObject(obj, {
const isAnnouncementsResponse = (obj: unknown): obj is AnnouncementsResponse => (
isObject(obj, {
'list_of_announcements': obj => isArrayOf<AnnouncementResponse>(obj, isAnnouncementResponse),
'Success': 'boolean'
})
})
)
export type {
AnnouncementsResponse,

View File

@ -3,10 +3,12 @@ import { OsmAddressResponse } from './types'
const initialOsmAddress = ''
const composeOsmAddressURL = (addressPosition: LatLng) =>
const composeOsmAddressURL = (addressPosition: LatLng) => (
`${location.protocol}//nominatim.openstreetmap.org/reverse?format=json&accept-language=ru&lat=${addressPosition.lat}&lon=${addressPosition.lng}`
)
const processOsmAddress = (data: OsmAddressResponse): string =>
const processOsmAddress = (data: OsmAddressResponse): string => (
data.display_name
)
export { initialOsmAddress, composeOsmAddressURL, processOsmAddress }

View File

@ -4,9 +4,11 @@ type OsmAddressResponse = {
display_name: string
}
const isOsmAddressResponse = (obj: unknown): obj is OsmAddressResponse => isObject(obj, {
const isOsmAddressResponse = (obj: unknown): obj is OsmAddressResponse => (
isObject(obj, {
'display_name': 'string',
})
})
)
export type {
OsmAddressResponse,

View File

@ -0,0 +1,12 @@
import { API_URL } from '../../config'
import { PutAnnouncement, PutAnnouncementResponse } from './types'
const composePutAnnouncementURL = () => (
API_URL + '/announcement?'
)
const processPutAnnouncement = (data: PutAnnouncementResponse): PutAnnouncement => {
return data.Answer
}
export { composePutAnnouncementURL, processPutAnnouncement }

View File

@ -0,0 +1,17 @@
import { isObject } from '../../utils/types'
type PutAnnouncementResponse = {
Answer: boolean
}
const isPutAnnouncementResponse = (obj: unknown): obj is PutAnnouncementResponse => (
isObject(obj, {
'Answer': 'boolean'
})
)
type PutAnnouncement = boolean
export type { PutAnnouncementResponse, PutAnnouncement }
export { isPutAnnouncementResponse }

View File

@ -1,11 +1,16 @@
import { LatLng } from 'leaflet'
import { API_URL } from '../../config'
import { Trashbox, TrashboxResponse } from './types'
const composeTrashboxURL = (position: LatLng) =>
const composeTrashboxURL = (position: LatLng) => (
API_URL + '/trashbox?' + new URLSearchParams({
lat: position.lat.toString(),
lng: position.lng.toString()
}).toString()
)
export { composeTrashboxURL }
const processTrashbox = (data: TrashboxResponse): Trashbox[] =>
data
export { composeTrashboxURL, processTrashbox }

View File

@ -7,16 +7,20 @@ type Trashbox = {
Categories: string[]
}
const isTrashbox = (obj: unknown): obj is Trashbox => isObject(obj, {
const isTrashbox = (obj: unknown): obj is Trashbox => (
isObject(obj, {
'Lat': 'number',
'Lng': 'number',
'Address': 'string',
'Categories': obj => isArrayOf<string>(obj, isString)
})
})
)
type TrashboxResponse = Trashbox[]
const isTrashboxResponse = (obj: unknown): obj is Trashbox[] => isArrayOf(obj, isTrashbox)
const isTrashboxResponse = (obj: unknown): obj is Trashbox[] => (
isArrayOf(obj, isTrashbox)
)
export type { Trashbox, TrashboxResponse }
export { isTrashbox, isTrashboxResponse }

View File

@ -4,7 +4,9 @@ const categories = ['PORRIDGE', 'conspects', 'milk', 'bred', 'wathing', 'cloth',
'fruits_vegatables', 'soup', 'dinner', 'conserves', 'pens', 'other_things'] as const
type Category = typeof categories[number]
const isCategory = (obj: unknown): obj is Category => isLiteralUnion(obj, categories)
const isCategory = (obj: unknown): obj is Category => (
isLiteralUnion(obj, categories)
)
const categoryGraphics: Record<Category, string> = {
'PORRIDGE': 'static/PORRIDGE.jpg',

View File

@ -101,9 +101,9 @@ const lineNames: Record<Lines, string> = {
violet: 'Фиолетовая',
}
const lineByName = (name: string) =>
const lineByName = (name: string) => (
lines.find(line => stations[line].has(name))
)
export type { Lines }
export { lines, stations, colors, lineNames, lineByName }

View File

@ -8,8 +8,9 @@ type AuthFormProps = {
error: string
}
const AuthForm = ({ handleAuth, register, loading, error }: AuthFormProps) => {
function AuthForm ({ handleAuth, register, loading, error }: AuthFormProps) {
const buttonText = loading ? 'Загрузка...' : (error || (register ? 'Зарегистрироваться' : 'Войти'))
return (
<Form onSubmit={handleAuth}>
<Form.Group className='mb-3' controlId='email'>

View File

@ -10,7 +10,7 @@ type LocationMarkerProps = {
setPosition: SetState<LatLng>
}
const LocationMarker = ({ address, position, setPosition }: LocationMarkerProps) => {
function LocationMarker({ address, position, setPosition }: LocationMarkerProps) {
const map = useMapEvents({
dragend: () => {

View File

@ -8,7 +8,7 @@ type TrashboxMarkersProps = {
selectTrashbox: ({ index, category }: { index: number, category: string }) => void
}
const TrashboxMarkers = ({ trashboxes, selectTrashbox }: TrashboxMarkersProps) => {
function TrashboxMarkers({ trashboxes, selectTrashbox }: TrashboxMarkersProps) {
return (
<>{trashboxes.map((trashbox, index) => (
<Marker icon={iconTrash} key={`${trashbox.Lat}${trashbox.Lng}`} position={[trashbox.Lat, trashbox.Lng]}>

View File

@ -1,77 +1,31 @@
import { useEffect, useRef, useState } from 'react'
import { API_URL } from '../../config'
import { isLiteralUnion } from '../../utils/types'
import { handleHTTPErrors } from '../../utils'
const addErrors = ['Не удалось опубликовать объявление', 'Неверный ответ от сервера', 'Неизвестная ошибка'] as const
type AddError = typeof addErrors[number]
const isAddError = (obj: unknown): obj is AddError => isLiteralUnion(obj, addErrors)
const buttonStates = ['Опубликовать', 'Загрузка...', 'Опубликовано', 'Отменено'] as const
type ButtonState = typeof buttonStates[number] | AddError
type AddResponse = {
Answer: boolean
}
const isAddResponse = (obj: unknown): obj is AddResponse =>
typeof obj === 'object' && obj !== null && typeof Reflect.get(obj, 'Answer') === 'boolean'
import { useCallback } from 'react'
import { useSend } from '..'
import { composePutAnnouncementURL, processPutAnnouncement } from '../../api/putAnnouncement'
import { isPutAnnouncementResponse } from '../../api/putAnnouncement/types'
import useSendButtonCaption from '../useSendButtonCaption'
const useAddAnnouncement = () => {
const [status, setStatus] = useState<ButtonState>('Опубликовать')
const { doSend, loading, error } = useSend(
composePutAnnouncementURL(),
'PUT',
true,
isPutAnnouncementResponse,
processPutAnnouncement,
)
const timerIdRef = useRef<number>()
const abortControllerRef = useRef<AbortController>()
const { update, ...button } = useSendButtonCaption('Опубликовать', loading, error, 'Опубликовано')
const doAdd = async (formData: FormData) => {
if (status === 'Загрузка...') {
abortControllerRef.current?.abort()
setStatus('Отменено')
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 3000)
return
}
setStatus('Загрузка...')
const abortController = new AbortController()
abortControllerRef.current = abortController
try {
const res = await fetch(API_URL + '/announcement', {
method: 'PUT',
body: formData,
signal: abortController.signal
const doSendWithButton = useCallback(async (formData: FormData) => {
const data = await doSend({}, {
body: formData
})
update(data)
handleHTTPErrors(res)
return data
}, [doSend, update])
const data: unknown = await res.json()
if (!isAddResponse(data)) throw new Error('Неверный ответ от сервера')
if (!data.Answer) {
throw new Error('Не удалось опубликовать объявление')
}
setStatus('Опубликовано')
} catch (err) {
setStatus(isAddError(err) ? err : 'Неизвестная ошибка')
timerIdRef.current = setTimeout(() => setStatus('Опубликовать'), 10000)
}
}
useEffect(() => {
const abortController = abortControllerRef.current
return () => {
clearTimeout(timerIdRef.current)
abortController?.abort()
}
})
return { doAdd, status }
return { doSend: doSendWithButton, button }
}
export default useAddAnnouncement

View File

@ -1,10 +1,10 @@
import useFetch from './useFetch'
import { useFetch } from '../'
import { FiltersType } from '../../utils/filters'
import { composeAnnouncementsURL, initialAnnouncements, processAnnouncements } from '../../api/announcements'
import { isAnnouncementsResponse } from '../../api/announcements/types'
const useAnnouncements = (filters: FiltersType) =>
const useAnnouncements = (filters: FiltersType) => (
useFetch(
composeAnnouncementsURL(filters),
'GET',
@ -13,6 +13,6 @@ const useAnnouncements = (filters: FiltersType) =>
processAnnouncements,
initialAnnouncements
)
)
export default useAnnouncements

View File

@ -38,16 +38,18 @@ interface LogInResponse {
token_type: 'bearer'
}
const isLogInResponse = (obj: unknown): obj is LogInResponse => isObject(obj, {
const isLogInResponse = (obj: unknown): obj is LogInResponse => (
isObject(obj, {
'access_token': 'string',
'token_type': isConst('bearer')
})
})
)
function useAuth() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const doAuth = async (data: AuthData, newAccount: boolean) => {
async function doAuth(data: AuthData, newAccount: boolean) {
setLoading(true)
if (newAccount) {

View File

@ -10,9 +10,11 @@ type BookResponse = {
Success: boolean
}
const isBookResponse = (obj: unknown): obj is BookResponse => isObject(obj, {
const isBookResponse = (obj: unknown): obj is BookResponse => (
isObject(obj, {
'Success': 'boolean'
})
})
)
type BookStatus = '' | 'Загрузка...' | 'Забронировано' | 'Ошибка бронирования'

View File

@ -1,128 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { handleHTTPErrors, isAborted } from '../../utils'
import { getToken } from '../../utils/auth'
import { useNavigate } from 'react-router-dom'
import { SetState } from '../../utils/types'
type UseFetchShared = {
loading: boolean,
abort?: () => void,
}
type UseFetchSucced<T> = {
error: null,
data: T,
} & UseFetchShared
type UseFetchErrored = {
error: string,
data: undefined
} & UseFetchShared
const gotError = <T>(res: UseFetchErrored | UseFetchSucced<T>): res is UseFetchErrored =>
typeof res.error === 'string'
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored) =>
gotError(res) ? res.error : res.data
type UseFetchReturn<T> = ({
error: null,
data: T
} | {
error: string,
data: undefined
}) & {
loading: boolean,
setData: SetState<T | undefined>
abort?: (() => void)
}
const useFetch = <R, T>(
url: string,
method: RequestInit['method'],
needAuth: boolean,
guardResponse: (data: unknown) => data is R,
processData: (data: R) => T,
initialData?: T,
params?: RequestInit
): UseFetchReturn<T> => {
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const navigate = useNavigate()
const abortControllerRef = useRef<AbortController>()
useEffect(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
const headers = new Headers({
...params?.headers
})
if (needAuth) {
const token = getToken()
if (token === null) {
return navigate('/login')
}
headers.append('Auth', `Bearer ${token}`)
}
fetch(url, {
method,
...params,
headers,
signal: abortControllerRef.current.signal,
})
.then(res => {
handleHTTPErrors(res)
return res.json()
})
.then(data => {
if (!guardResponse(data)) {
throw new Error('Malformed server response')
}
setData(processData(data))
setLoading(false)
})
.catch(err => {
if (err instanceof Error && !isAborted(err)) {
setError('Ошибка сети')
if (import.meta.env.DEV) {
console.log(url, params, err)
}
}
setLoading(false)
})
return () => abortControllerRef.current?.abort()
}, [url, method, needAuth, params, guardResponse, processData, navigate])
return {
...(
error === null ? ({
data: data!, error: null
}) : ({ data: undefined, error })
),
loading,
setData,
abort: abortControllerRef.current?.abort.bind(abortControllerRef.current)
}
}
export default useFetch
export { gotError, fallbackError }

View File

@ -1,10 +1,10 @@
import { LatLng } from 'leaflet'
import useFetch from './useFetch'
import { useFetch } from '../'
import { composeOsmAddressURL, processOsmAddress } from '../../api/osmAddress'
import { isOsmAddressResponse } from '../../api/osmAddress/types'
const useOsmAddresses = (addressPosition: LatLng) =>
const useOsmAddresses = (addressPosition: LatLng) => (
useFetch(
composeOsmAddressURL(addressPosition),
'GET',
@ -13,5 +13,6 @@ const useOsmAddresses = (addressPosition: LatLng) =>
processOsmAddress,
''
)
)
export default useOsmAddresses

View File

@ -1,18 +1,18 @@
import { LatLng } from 'leaflet'
import useFetch from './useFetch'
import { composeTrashboxURL } from '../../api/trashbox'
import { useFetch } from '../'
import { composeTrashboxURL, processTrashbox } from '../../api/trashbox'
import { isTrashboxResponse } from '../../api/trashbox/types'
const useTrashboxes = (position: LatLng) =>
const useTrashboxes = (position: LatLng) => (
useFetch(
composeTrashboxURL(position),
'GET',
true,
isTrashboxResponse,
(data) => data,
processTrashbox,
[]
)
)
export default useTrashboxes

View File

@ -1 +1,3 @@
export { default as useStoryDimensions } from './useStoryDimensions'
export { default as useSend } from './useSend'
export { default as useFetch } from './useFetch'

View File

@ -0,0 +1,75 @@
import { useEffect, useState } from 'react'
import { SetState } from '../utils/types'
import useSend from './useSend'
type UseFetchShared = {
loading: boolean,
abort?: () => void,
}
type UseFetchSucced<T> = {
error: null,
data: T,
} & UseFetchShared
type UseFetchErrored = {
error: string,
data: undefined
} & UseFetchShared
const gotError = <T>(res: UseFetchErrored | UseFetchSucced<T>): res is UseFetchErrored => (
typeof res.error === 'string'
)
const fallbackError = <T>(res: UseFetchSucced<T> | UseFetchErrored) => (
gotError(res) ? res.error : res.data
)
type UseFetchReturn<T> = ({
error: null,
data: T
} | {
error: string,
data: undefined
}) & {
loading: boolean,
setData: SetState<T | undefined>
abort?: (() => void)
}
function useFetch<R, T>(
url: string,
method: RequestInit['method'],
needAuth: boolean,
guardResponse: (data: unknown) => data is R,
processResponse: (data: R) => T,
initialData?: T,
params?: Omit<RequestInit, 'method'>
): UseFetchReturn<T> {
const [data, setData] = useState(initialData)
const { doSend, loading, error } = useSend(url, method, needAuth, guardResponse, processResponse, params)
useEffect(() => {
doSend().then(
data => { if (data !== undefined) setData(data) }
).catch( // must never occur
err => import.meta.env.DEV && console.error('Failed to do fetch request', err)
)
}, [doSend])
return {
...(
error === null ? ({
data: data!, error: null
}) : ({ data: undefined, error })
),
loading,
setData
}
}
export default useFetch
export { gotError, fallbackError }

View File

@ -0,0 +1,95 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { getToken } from '../utils/auth'
import { handleHTTPErrors, isAborted } from '../utils'
function useSend<R, T>(
url: string,
method: RequestInit['method'],
needAuth: boolean,
guardResponse: (data: unknown) => data is R,
processResponse: (data: R) => T,
defaultParams?: Omit<RequestInit, 'method'>
) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const navigate = useNavigate()
const abortControllerRef = useRef<AbortController>()
useEffect(() => () => abortControllerRef.current?.abort(), [])
/** Don't use in useEffect. If you need request result, go with useFetch instead */
const doSend = useCallback(async (urlProps?: Record<string, string>, params?: Omit<RequestInit, 'method'>) => {
setLoading(true)
setError(null)
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
const abortController = new AbortController()
abortControllerRef.current = abortController
const headers = new Headers({
...defaultParams?.headers,
...params?.headers
})
if (needAuth) {
const token = getToken()
if (token === null) {
navigate('/login')
return undefined
}
headers.append('Auth', `Bearer ${token}`)
}
try {
const res = await fetch(url + new URLSearchParams(urlProps).toString(), {
method,
...defaultParams,
...params,
headers,
signal: abortControllerRef.current.signal,
})
handleHTTPErrors(res)
const data: unknown = await res.json()
if (!guardResponse(data)) {
throw new Error('Malformed server response')
}
setLoading(false)
return processResponse(data)
} catch (err) {
if (err instanceof Error && !isAborted(err)) {
setError('Ошибка сети')
if (import.meta.env.DEV) {
console.log(url, params, err)
}
}
setLoading(false)
return undefined
}
}, [defaultParams, needAuth, navigate, url, method, guardResponse, processResponse])
return {
doSend, loading, error,
abort: abortControllerRef.current?.abort.bind(abortControllerRef.current)
}
}
export default useSend

View File

@ -0,0 +1,44 @@
import { useCallback, useEffect, useState } from 'react'
function useSendButtonCaption(
initial: string,
loading: boolean,
error: string | null,
result = initial,
singular = true
) {
const [caption, setCaption] = useState(initial)
const [disabled, setDisabled] = useState(false)
const [title, setTitle] = useState(initial)
const update = useCallback(<T extends NonNullable<unknown>>(data: T | undefined) => {
if (data !== undefined) {
setCaption(result)
setTitle('Отправить ещё раз')
if (singular) {
setDisabled(true)
setTitle('')
}
}
}, [result, singular])
useEffect(() => {
if (loading) {
setCaption('Загрузка...')
setTitle('Отменить и отправить ещё раз')
}
}, [loading])
useEffect(() => {
if (!loading && error !== null) {
setCaption(error + ', нажмите, чтобы попробовать ещё раз')
setTitle('')
}
}, [error, loading])
return { update, children: caption, disabled, title }
}
export default useSendButtonCaption

View File

@ -1,15 +1,13 @@
import { useState, useEffect } from 'react';
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
const getWindowDimensions = () => (
{
width: window.innerWidth,
height: window.innerHeight
}
)
return {
width,
height
};
}
function useStoryDimensions(maxRatio = 16/9) {
function useStoryDimensions(maxRatio = 16 / 9) {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
useEffect(() => {

View File

@ -1,14 +1,13 @@
import { CSSProperties, FormEventHandler, useEffect, useState } from 'react'
import { CSSProperties, FormEventHandler, useState } from 'react'
import { Form, Button, Card } from 'react-bootstrap'
import { MapContainer, TileLayer } from 'react-leaflet'
import { latLng } from 'leaflet'
import { ClickHandler, LocationMarker, TrashboxMarkers } from '../components'
import { useAddAnnouncement, useTrashboxes } from '../hooks/api'
import { handleHTTPErrors } from '../utils'
import { categories, categoryNames } from '../assets/category'
import { stations, lines, lineNames } from '../assets/metro'
import { fallbackError, gotError } from '../hooks/api/useFetch'
import { fallbackError, gotError } from '../hooks/useFetch'
import { useOsmAddresses } from '../hooks/api'
const styles = {
@ -32,25 +31,7 @@ function AddPage() {
const address = useOsmAddresses(addressPosition)
useEffect(() => {
if (!gotError(address))
void (async () => {
try {
const res = await fetch(location.protocol + '//nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(address.data))
handleHTTPErrors(res)
const fetchData: unknown = await res.json()
console.log('f', fetchData)
} catch (err) {
console.error(err)
}
})()
}, [address])
const { doAdd, status } = useAddAnnouncement()
const { doSend, button } = useAddAnnouncement()
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
@ -63,7 +44,7 @@ function AddPage() {
formData.append('address', address.data || '') // if address.error
formData.set('bestBy', new Date((formData.get('bestBy') as number | null) || 0).getTime().toString())
void doAdd(formData)
void doSend(formData)
}
return (
@ -151,7 +132,7 @@ function AddPage() {
</Form.Select>
</Form.Group>
<Form.Group className='mb-3' controlId='password'>
<Form.Group className='mb-3' controlId='trashbox'>
<Form.Label>Пункт сбора мусора</Form.Label>
<div className='mb-3'>
{trashboxes.loading
@ -195,9 +176,7 @@ function AddPage() {
)}
</Form.Group>
<Button variant='success' type='submit'>
{status}
</Button>
<Button variant='success' type='submit' {...button} />
</Form>
</Card.Body>
</Card>

View File

@ -10,7 +10,7 @@ import { Announcement } from '../api/announcement/types'
import { categoryGraphics } from '../assets/category'
import puffSpinner from '../assets/puff.svg'
import { gotError } from '../hooks/api/useFetch'
import { gotError } from '../hooks/useFetch'
function generateStories(announcements: Announcement[]): Story[] {
return announcements.map(announcement => {
@ -63,7 +63,7 @@ const styles = {
}
function HomePage() {
const { height, width } = useStoryDimensions(16 / 10)
const { height, width } = useStoryDimensions(16 / 9)
const [filterShown, setFilterShown] = useState(false)
const [filter, setFilter] = useState(defaultFilters)

View File

@ -28,7 +28,7 @@ function LoginPage() {
if (token) {
setToken(token)
navigate('/')
navigate(-1 - Number(import.meta.env.DEV))
}
}

View File

@ -6,7 +6,7 @@ const getToken = () => {
return token
}
const setToken = (token: string) => {
function setToken(token: string) {
localStorage.setItem('Token', token)
}

View File

@ -7,10 +7,12 @@ type FiltersType = Partial<Pick<Announcement, FilterNames>>
const defaultFilters: FiltersType = { userId: undefined, category: undefined, metro: undefined, bookedBy: undefined }
const URLEncodeFilters = (filters: FiltersType) => Object.fromEntries(
const URLEncodeFilters = (filters: FiltersType) => (
Object.fromEntries(
filterNames.map(
fName => [fName, filters[fName]?.toString()]
).filter((p): p is [string, string] => typeof p[1] !== 'undefined')
)
)
export type { FilterNames, FiltersType }

View File

@ -1,6 +1,8 @@
const isAborted = (err: Error) => err.name === 'AbortError'
const isAborted = (err: Error) => (
err.name === 'AbortError'
)
const handleHTTPErrors = (res: Response) => {
function handleHTTPErrors(res: Response) {
if (!res.ok) {
switch (res.status) {
case 401:

View File

@ -20,7 +20,9 @@ const isObject = <T>(obj: unknown, properties: PropertiesGuards): obj is T => (
Object.entries(properties).every(([name, guard]) => {
const param: unknown = Reflect.get(obj, name)
console.log(name, param, guard)
if (import.meta.env.DEV) {
console.debug('isObject', name, param, guard)
}
if (typeof guard === 'function') {
return guard(param)
@ -54,7 +56,9 @@ const isArrayOf = <T>(obj: unknown, itemGuard: ((obj: unknown) => obj is T)): ob
obj.every(itemGuard)
)
const isString = (obj: unknown): obj is string => typeof obj === 'string'
const isString = (obj: unknown): obj is string => (
typeof obj === 'string'
)
type SetState<T> = React.Dispatch<React.SetStateAction<T>>

View File

@ -9,8 +9,9 @@ import { ${NAME}Response, ${NAME} } from './types'
const initial${NAME}: ${NAME} = {}
const compose${NAME}URL = () =>
const compose${NAME}URL = () => (
API_URL + '/${NAME,}?'
)
const process${NAME} = (data: ${NAME}Response): ${NAME} => {
return data
@ -26,19 +27,21 @@ type ${NAME}Response = {
}
const is${NAME}Response = (obj: unknown): obj is ${NAME}Response =>
const is${NAME}Response = (obj: unknown): obj is ${NAME}Response => (
isObject(obj, {
})
)
type ${NAME} = {
}
const is${NAME} = (obj: unknown): obj is ${NAME} =>
const is${NAME} = (obj: unknown): obj is ${NAME} => (
isObject(obj, {
})
)
export type { ${NAME}Response, ${NAME} }

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

85
migrations/env.py Normal file
View File

@ -0,0 +1,85 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from back import db, base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = base.Base.metadata
# target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
# url = config.get_main_option("sqlalchemy.url")
url = config.get_main_option(db.SQLALCHEMY_DATABASE_URL)
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
configuration = config.get_section(config.config_ini_section)
configuration['sqlalchemy.url'] = db.SQLALCHEMY_DATABASE_URL
connectable = engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,28 @@
"""first
Revision ID: 0006eca30e2c
Revises:
Create Date: 2023-07-23 22:32:43.496939
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0006eca30e2c'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""Poems table added
Revision ID: 18001c2231e3
Revises: 33c5716276b5
Create Date: 2023-07-23 22:50:16.055961
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '18001c2231e3'
down_revision = '33c5716276b5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('poems',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('poem_text', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_poems_id'), 'poems', ['id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_poems_id'), table_name='poems')
op.drop_table('poems')
# ### end Alembic commands ###

View File

@ -0,0 +1,70 @@
"""Try to make alembic see models
Revision ID: 33c5716276b5
Revises: 0006eca30e2c
Create Date: 2023-07-23 22:42:07.532395
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '33c5716276b5'
down_revision = '0006eca30e2c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('announcements',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(), nullable=True),
sa.Column('category', sa.String(), nullable=True),
sa.Column('best_by', sa.Integer(), nullable=True),
sa.Column('address', sa.String(), nullable=True),
sa.Column('longtitude', sa.Integer(), nullable=True),
sa.Column('latitude', sa.Integer(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('src', sa.String(), nullable=True),
sa.Column('metro', sa.String(), nullable=True),
sa.Column('trashId', sa.Integer(), nullable=True),
sa.Column('booked_by', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_announcements_id'), 'announcements', ['id'], unique=False)
op.create_table('trashboxes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('address', sa.String(), nullable=True),
sa.Column('latitude', sa.Integer(), nullable=True),
sa.Column('longtitude', sa.Integer(), nullable=True),
sa.Column('category', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_trashboxes_id'), 'trashboxes', ['id'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('phone', sa.Integer(), nullable=True),
sa.Column('email', sa.String(), nullable=True),
sa.Column('password', sa.String(), nullable=True),
sa.Column('hashed_password', sa.String(), nullable=True),
sa.Column('name', sa.String(), nullable=True),
sa.Column('surname', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_table('users')
op.drop_index(op.f('ix_trashboxes_id'), table_name='trashboxes')
op.drop_table('trashboxes')
op.drop_index(op.f('ix_announcements_id'), table_name='announcements')
op.drop_table('announcements')
# ### end Alembic commands ###