Compare commits

...

4 commits

25 changed files with 373 additions and 204 deletions

View file

@ -1,7 +1,7 @@
DB_USER=
DB_PASS=
DB_HOST= DB_HOST=
DB_PORT= DB_PORT=
DB_USER=
DB_PASS=
DB_NAME= DB_NAME=
SECRET_KEY= SECRET_KEY=

View file

@ -1,4 +1,4 @@
from pydantic import BaseSettings from pydantic_settings import BaseSettings
class Settings(BaseSettings): class Settings(BaseSettings):

View file

@ -54,3 +54,13 @@ class IncorrectLengthOfNicknameException(BlackPhoenixException):
detail = "Ник должен быть не короче 2 и не длиннее 30 символов" detail = "Ник должен быть не короче 2 и не длиннее 30 символов"
class UDontHavePermissionException(BlackPhoenixException):
status_code = status.HTTP_409_CONFLICT
detail = "У вас нет прав для этого действия"
class MessageNotFoundException(BlackPhoenixException):
status_code = status.HTTP_404_NOT_FOUND
detail = "Сообщение не найдено"

View file

@ -1,12 +1,17 @@
from fastapi import FastAPI from fastapi import FastAPI
from starlette.staticfiles import StaticFiles
from app.users.chat.router import router as chat_router from app.users.chat.router import router as chat_router
from app.users.router import router as user_router from app.users.router import router as user_router
from app.pages.router import router as pages_router
app = FastAPI() app = FastAPI()
app.include_router(chat_router) app.include_router(chat_router)
app.include_router(user_router) app.include_router(user_router)
app.include_router(pages_router)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
@app.get('/') @app.get('/')

View file

@ -5,15 +5,15 @@ from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from alembic import context from alembic import context
sys.path.insert(0, dirname(dirname(abspath(__file__))))
from app.database import DATABASE_URL, Base from app.database import DATABASE_URL, Base
from app.users.models import Users # noqa from app.users.models import Users # noqa
from app.users.chat.models import Chats # noqa from app.users.chat.models import Chats, Messages, UsersXChats # noqa
# 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.
sys.path.insert(0, dirname(dirname(abspath(__file__))))
config = context.config config = context.config
config.set_main_option('sqlalchemy.url', f'{DATABASE_URL}?async_fallback=True') config.set_main_option('sqlalchemy.url', f'{DATABASE_URL}?async_fallback=True')
@ -29,6 +29,7 @@ if config.config_file_name is not None:
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
# can be acquired: # can be acquired:
# my_important_option = config.get_main_option("my_important_option") # my_important_option = config.get_main_option("my_important_option")

View file

@ -0,0 +1,69 @@
"""Изменение models.py
Revision ID: 2913a8a70afb
Revises:
Create Date: 2024-02-01 14:31:04.662656
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '2913a8a70afb'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('role', sa.Integer(), nullable=False),
sa.Column('black_phoenix', sa.Integer(), nullable=False),
sa.Column('avatar_image', sa.String(), server_default='app/static/images/ту уже пешка BP.png', nullable=True),
sa.Column('date_of_birth', sa.Date(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('chats',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chat_for', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['chat_for'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('messages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('message', sa.String(), nullable=False),
sa.Column('image_url', sa.String(), nullable=True),
sa.Column('chat_id', sa.Integer(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('visibility', sa.Boolean(), server_default='true', nullable=False),
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('usersxchats',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('chat_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('usersxchats')
op.drop_table('messages')
op.drop_table('chats')
op.drop_table('users')
# ### end Alembic commands ###

View file

@ -1,32 +0,0 @@
"""Убрал обязательность авы
Revision ID: 3eb642de804e
Revises: 43eac1ddf80a
Create Date: 2024-01-29 19:37:41.136288
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '3eb642de804e'
down_revision: Union[str, None] = '43eac1ddf80a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('avatar_image', sa.String(), nullable=True))
op.drop_column('users', 'image')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('image', sa.VARCHAR(), autoincrement=False, nullable=False))
op.drop_column('users', 'avatar_image')
# ### end Alembic commands ###

View file

@ -1,30 +0,0 @@
"""Добавил колонку юзерам
Revision ID: 43eac1ddf80a
Revises: 90665e133296
Create Date: 2024-01-29 19:35:47.727712
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '43eac1ddf80a'
down_revision: Union[str, None] = '90665e133296'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('date_of_birth', sa.Date(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'date_of_birth')
# ### end Alembic commands ###

View file

@ -1,30 +0,0 @@
"""Добавил колонку юзерам
Revision ID: 90665e133296
Revises: 9845ad4fed24
Create Date: 2024-01-29 18:50:09.853356
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '90665e133296'
down_revision: Union[str, None] = '9845ad4fed24'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('image', sa.String(), nullable=False))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'image')
# ### end Alembic commands ###

View file

@ -1,45 +0,0 @@
"""Database Creation
Revision ID: 9845ad4fed24
Revises: e434e2885475
Create Date: 2024-01-29 18:32:13.361975
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '9845ad4fed24'
down_revision: Union[str, None] = 'e434e2885475'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chats',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chat', sa.JSON(), nullable=True),
sa.Column('allowed_users', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('role', sa.Integer(), nullable=False),
sa.Column('black_phoenix', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users')
op.drop_table('chats')
# ### end Alembic commands ###

View file

@ -1,30 +0,0 @@
"""Database Creation
Revision ID: e434e2885475
Revises:
Create Date: 2024-01-29 18:30:03.314179
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e434e2885475'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

14
app/pages/router.py Normal file
View file

@ -0,0 +1,14 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
router = APIRouter(
prefix="/pages",
tags=["Страницы"]
)
templates = Jinja2Templates(directory="app/templates")
@router.get("/base")
async def base(request: Request):
return templates.TemplateResponse("base.html", {"request": request})

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

36
app/templates/base.html Normal file
View file

@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
{% block head %}{% endblock %}
<title>BlackPhoenix</title>
</head>
<body>
<nav class="flex justify-between text-3xl my-3">
<ul>
<li class="BpMainButton"><button>Black Phoenix</button></li>
<li class="BpMyAccount"><button>Мой аккаунт</button></li>
</ul>
<style>
ul,li{
display: inline;
}
.BpMainButton{
float: left;
}
.BpMyAccount{
float: right;
}
</style>
</nav>
<hr>
<div id="content">
{% block content %}{% endblock %}
</div>
</body>
</html>

View file

@ -1,7 +1,65 @@
from sqlalchemy import insert, select, update, and_
from app.dao.base import BaseDAO from app.dao.base import BaseDAO
from app.database import async_session_maker
from app.users.models import Users from app.users.models import Users
from app.users.chat.models import Chats from app.users.chat.models import Chats, Messages
class ChatDAO(BaseDAO): class ChatDAO(BaseDAO):
model = Chats model = Chats
@classmethod
async def create(cls, user_id):
query = insert(Chats).values(chat_for=user_id)
async with async_session_maker() as session:
await session.execute(query)
await session.commit()
return True
@classmethod
async def send_message(cls, user_id, chat_id, message, image_url):
query = insert(Messages).values(chat_id=chat_id, user_id=user_id, message=message, image_url=image_url)
async with async_session_maker() as session:
await session.execute(query)
await session.commit()
return True
@classmethod
async def get_message_by_id(cls, message_id):
query = select(Messages.__table__.columns).where(
and_(
Messages.id == message_id,
Messages.visibility == True
)
)
async with async_session_maker() as session:
result = await session.execute(query)
result = result.mappings().all()
if result:
return result[0]
@classmethod
async def get_last_message(cls, chat_id):
query = select(Messages.__table__.columns).where(
and_(
Messages.chat_id == chat_id,
Messages.visibility == True
)
).order_by(Messages.created_at.desc()).limit(1)
async with async_session_maker() as session:
result = await session.execute(query)
result = result.mappings().all()
if result:
return result[0]
@classmethod
async def delete_message(cls, message_id):
query = update(Messages).where(Messages.id == message_id).values(visibility=False)
async with async_session_maker() as session:
await session.execute(query)
await session.commit()
return True

View file

@ -1,5 +1,8 @@
from sqlalchemy import Column, Integer, JSON from datetime import datetime
from typing import Optional
from sqlalchemy import func, ForeignKey, DateTime
from sqlalchemy.orm import mapped_column, Mapped, relationship
from app.database import Base from app.database import Base
@ -7,9 +10,44 @@ from app.database import Base
class Chats(Base): class Chats(Base):
__tablename__ = "chats" __tablename__ = "chats"
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
chat = Column(JSON) chat_for = mapped_column(ForeignKey("users.id"))
allowed_users = Column(JSON)
message = relationship("Messages", back_populates="chat")
usersxchats = relationship("UsersXChats", back_populates="chat")
user_to_exclude = relationship("Users", back_populates="chat")
def __str__(self): def __str__(self):
return f"Чат #{self.id} с {self.allowed_users}." return f"Чат #{self.id}."
class Messages(Base):
__tablename__ = "messages"
id: Mapped[int] = mapped_column(primary_key=True)
chat_id = mapped_column(ForeignKey("chats.id"))
user_id = mapped_column(ForeignKey("users.id"))
message: Mapped[str]
image_url: Mapped[Optional[str]]
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
visibility: Mapped[bool] = mapped_column(server_default='true')
chat = relationship("Chats", back_populates="message")
user = relationship("Users", back_populates="message")
def __str__(self):
return f"#{self.id} {self.text} от {self.user_id}. Написано {self.created_at}"
class UsersXChats(Base):
__tablename__ = "usersxchats"
id: Mapped[int] = mapped_column(primary_key=True)
user_id = mapped_column(ForeignKey("users.id"))
chat_id = mapped_column(ForeignKey("chats.id"))
chat = relationship("Chats", back_populates="usersxchats")
user = relationship("Users", back_populates="usersxchats")
def __str__(self):
return f"Юзер #{self.user_id} допущен к чату {self.chat_id}"

View file

@ -1,7 +1,13 @@
from fastapi import APIRouter, WebSocket from fastapi import APIRouter, WebSocket, Depends, status
from starlette.websockets import WebSocketDisconnect from starlette.websockets import WebSocketDisconnect
from app.exceptions import UDontHavePermissionException, MessageNotFoundException
from app.users.chat.dao import ChatDAO
from app.users.chat.shemas import SMessage
from app.users.chat.websocket import manager from app.users.chat.websocket import manager
from app.users.dao import UserDAO
from app.users.dependencies import get_current_user
from app.users.models import Users
router = APIRouter( router = APIRouter(
prefix="/chat", prefix="/chat",
@ -9,18 +15,65 @@ router = APIRouter(
) )
@router.get("") @router.post("", status_code=status.HTTP_201_CREATED)
async def root(): async def add_message_to_chat(
pass chat_id: int,
message: str,
image_url: str = None,
user: Users = Depends(get_current_user)
):
send_message_to_chat = await ChatDAO.send_message(
user_id=user.id,
chat_id=chat_id,
message=message,
image_url=image_url
)
@router.websocket("/ws/{user_id}") @router.delete("/delete_message", status_code=status.HTTP_204_NO_CONTENT)
async def websocket_endpoint(websocket: WebSocket, user_id: int): async def delete_message_from_chat(
message_id: int,
user: Users = Depends(get_current_user)
):
get_message_sender = await ChatDAO.get_message_by_id(message_id=message_id)
if get_message_sender is None:
raise MessageNotFoundException
if get_message_sender["user_id"] != user.id:
get_user_role = await UserDAO.get_user_role(user_id=user.id)
if not get_user_role == 1:
raise UDontHavePermissionException
deleted_message = await ChatDAO.delete_message(message_id=message_id)
return deleted_message
@router.get("/get_last_message", response_model=SMessage)
async def get_last_message(chat_id: int, user: Users = Depends(get_current_user)):
message = dict(await ChatDAO.get_last_message(chat_id=chat_id))
user_avatar = await UserDAO.get_user_avatar(user_id=user.id)
message["user_avatar"] = user_avatar
print(message)
if message is not None:
return message
raise MessageNotFoundException
@router.post("/create_chat")
async def create_chat(
user_to_exclude: int,
user: Users = Depends(get_current_user)
):
created_chat = await ChatDAO.create(user_id=user_to_exclude)
return created_chat
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, user: Users = Depends(get_current_user)):
await manager.connect(websocket) await manager.connect(websocket)
try: try:
while True: while True:
data = await websocket.receive_text() data = await websocket.receive_text()
await manager.broadcast(f"User {user_id}: {data}")
await manager.broadcast(f"User {user.id}: {data}")
except WebSocketDisconnect: except WebSocketDisconnect:
manager.disconnect(websocket) manager.disconnect(websocket)
await manager.broadcast(f"User {user_id}: себался") await manager.broadcast(f"User {user.id}: себался")

16
app/users/chat/shemas.py Normal file
View file

@ -0,0 +1,16 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class SMessage(BaseModel):
message: str
image_url: Optional[str] = None
chat_id: int
user_id: int
created_at: datetime
user_avatar: str
class Config:
from_attributes = True

View file

@ -3,6 +3,7 @@ from sqlalchemy.exc import SQLAlchemyError
from app.dao.base import BaseDAO from app.dao.base import BaseDAO
from app.database import async_session_maker from app.database import async_session_maker
from app.users.chat.models import UsersXChats
from app.users.models import Users from app.users.models import Users
@ -19,3 +20,24 @@ class UserDAO(BaseDAO):
result = await session.execute(query) result = await session.execute(query)
return result.scalar() return result.scalar()
@classmethod
async def get_user_role(cls, user_id: int):
query = select(Users.role).where(Users.id == user_id)
async with async_session_maker() as session:
result = await session.execute(query)
return result.scalar()
@classmethod
async def get_user_rights(cls, user_id: int):
query = select(UsersXChats.__table__.columns).where(UsersXChats.user_id == user_id)
async with async_session_maker() as session:
result = await session.execute(query)
print(result)
return result.mappings().all()
@classmethod
async def get_user_avatar(cls, user_id: int):
query = select(Users.avatar_image).where(Users.id == user_id)
async with async_session_maker() as session:
result = await session.execute(query)
return result.scalar()

View file

@ -8,10 +8,11 @@ from app.exceptions import (IncorrectTokenFormatException,
TokenAbsentException, TokenExpiredException, TokenAbsentException, TokenExpiredException,
UserIsNotPresentException) UserIsNotPresentException)
from app.users.dao import UserDAO from app.users.dao import UserDAO
from app.users.models import Users
def get_token(request: Request): def get_token(request: Request):
token = request.cookies.get("booking_access_token") token = request.cookies.get("black_phoenix_access_token")
if not token: if not token:
raise TokenAbsentException raise TokenAbsentException
return token return token
@ -34,3 +35,7 @@ async def get_current_user(token: str = Depends(get_token)):
if not user: if not user:
raise UserIsNotPresentException raise UserIsNotPresentException
return user return user
async def validate_user_rights(user: Users = Depends(get_current_user)): # Надо дописать
user_rights = UserDAO.get_user_rights()

View file

@ -1,4 +1,7 @@
from sqlalchemy import Column, Integer, String, Date from datetime import date
from typing import Optional
from sqlalchemy.orm import mapped_column, Mapped, relationship
from app.database import Base from app.database import Base
@ -6,14 +9,18 @@ from app.database import Base
class Users(Base): class Users(Base):
__tablename__ = "users" __tablename__ = "users"
id = Column(Integer, primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
email = Column(String, nullable=False) email: Mapped[str]
username = Column(String, nullable=False) username: Mapped[str]
hashed_password = Column(String, nullable=False) hashed_password: Mapped[str]
role = Column(Integer, nullable=False) role: Mapped[int]
black_phoenix = Column(Integer, nullable=False) black_phoenix: Mapped[int]
avatar_image = Column(String) avatar_image: Mapped[Optional[str]] = mapped_column(server_default='app/static/images/ту уже пешка BP.png')
date_of_birth = Column(Date, nullable=False) date_of_birth: Mapped[date]
message = relationship("Messages", back_populates="user")
usersxchats = relationship("UsersXChats", back_populates="user")
chat = relationship("Chats", back_populates="user_to_exclude")
def __str__(self): def __str__(self):
return f"Юзер {self.username}" return f"Юзер {self.username}"

View file

@ -40,7 +40,7 @@ async def register_user(response: Response, user_data: SUserRegister):
role=0, black_phoenix=0) role=0, black_phoenix=0)
user = await authenticate_user_by_email(user_data.email, user_data.password) user = await authenticate_user_by_email(user_data.email, user_data.password)
access_token = create_access_token({"sub": str(user.id)}) access_token = create_access_token({"sub": str(user.id)})
response.set_cookie("booking_access_token", access_token, httponly=True) response.set_cookie("black_phoenix_access_token", access_token, httponly=True)
return {"access_token": access_token} return {"access_token": access_token}
@ -52,13 +52,13 @@ async def login_user(response: Response, user_data: SUserLogin):
if not user: if not user:
raise IncorrectAuthDataException raise IncorrectAuthDataException
access_token = create_access_token({"sub": str(user.id)}) access_token = create_access_token({"sub": str(user.id)})
response.set_cookie("booking_access_token", access_token, httponly=True) response.set_cookie("black_phoenix_access_token", access_token, httponly=True)
return {"access_token": access_token} return {"access_token": access_token}
@router.post("/logout") @router.post("/logout")
async def logout_user(response: Response): async def logout_user(response: Response):
response.delete_cookie("booking_access_token") response.delete_cookie("black_phoenix_access_token")
@router.get("/me") @router.get("/me")

View file

@ -8,7 +8,7 @@ class SUserLogin(BaseModel):
password: str password: str
class Config: class Config:
orm_mode = True from_attributes = True
class SUserRegister(BaseModel): class SUserRegister(BaseModel):
@ -18,5 +18,5 @@ class SUserRegister(BaseModel):
date_of_birth: date date_of_birth: date
class Config: class Config:
orm_mode = True from_attributes = True

View file

@ -60,8 +60,10 @@ prometheus-fastapi-instrumentator==6.1.0
prompt-toolkit==3.0.43 prompt-toolkit==3.0.43
pyasn1==0.5.1 pyasn1==0.5.1
pycodestyle==2.11.1 pycodestyle==2.11.1
pydantic==1.10.14 pydantic==2.6.0
pydantic_core==2.14.6 pydantic-extra-types==2.5.0
pydantic-settings==2.1.0
pydantic_core==2.16.1
pyflakes==3.2.0 pyflakes==3.2.0
pyright==1.1.347 pyright==1.1.347
pytest==7.4.4 pytest==7.4.4