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_PORT=
DB_USER=
DB_PASS=
DB_NAME=
SECRET_KEY=

View file

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

View file

@ -54,3 +54,13 @@ class IncorrectLengthOfNicknameException(BlackPhoenixException):
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 starlette.staticfiles import StaticFiles
from app.users.chat.router import router as chat_router
from app.users.router import router as user_router
from app.pages.router import router as pages_router
app = FastAPI()
app.include_router(chat_router)
app.include_router(user_router)
app.include_router(pages_router)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
@app.get('/')

View file

@ -5,15 +5,15 @@ from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
sys.path.insert(0, dirname(dirname(abspath(__file__))))
from app.database import DATABASE_URL, Base
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
# access to the values within the .ini file in use.
sys.path.insert(0, dirname(dirname(abspath(__file__))))
config = context.config
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 = Base.metadata
# 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")

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.database import async_session_maker
from app.users.models import Users
from app.users.chat.models import Chats
from app.users.chat.models import Chats, Messages
class ChatDAO(BaseDAO):
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
@ -7,9 +10,44 @@ from app.database import Base
class Chats(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True)
chat = Column(JSON)
allowed_users = Column(JSON)
id: Mapped[int] = mapped_column(primary_key=True)
chat_for = mapped_column(ForeignKey("users.id"))
message = relationship("Messages", back_populates="chat")
usersxchats = relationship("UsersXChats", back_populates="chat")
user_to_exclude = relationship("Users", back_populates="chat")
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 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.dao import UserDAO
from app.users.dependencies import get_current_user
from app.users.models import Users
router = APIRouter(
prefix="/chat",
@ -9,18 +15,65 @@ router = APIRouter(
)
@router.get("")
async def root():
pass
@router.post("", status_code=status.HTTP_201_CREATED)
async def add_message_to_chat(
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}")
async def websocket_endpoint(websocket: WebSocket, user_id: int):
@router.delete("/delete_message", status_code=status.HTTP_204_NO_CONTENT)
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)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"User {user_id}: {data}")
await manager.broadcast(f"User {user.id}: {data}")
except WebSocketDisconnect:
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.database import async_session_maker
from app.users.chat.models import UsersXChats
from app.users.models import Users
@ -19,3 +20,24 @@ class UserDAO(BaseDAO):
result = await session.execute(query)
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,
UserIsNotPresentException)
from app.users.dao import UserDAO
from app.users.models import Users
def get_token(request: Request):
token = request.cookies.get("booking_access_token")
token = request.cookies.get("black_phoenix_access_token")
if not token:
raise TokenAbsentException
return token
@ -34,3 +35,7 @@ async def get_current_user(token: str = Depends(get_token)):
if not user:
raise UserIsNotPresentException
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
@ -6,14 +9,18 @@ from app.database import Base
class Users(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, nullable=False)
username = Column(String, nullable=False)
hashed_password = Column(String, nullable=False)
role = Column(Integer, nullable=False)
black_phoenix = Column(Integer, nullable=False)
avatar_image = Column(String)
date_of_birth = Column(Date, nullable=False)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str]
username: Mapped[str]
hashed_password: Mapped[str]
role: Mapped[int]
black_phoenix: Mapped[int]
avatar_image: Mapped[Optional[str]] = mapped_column(server_default='app/static/images/ту уже пешка BP.png')
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):
return f"Юзер {self.username}"

View file

@ -40,7 +40,7 @@ async def register_user(response: Response, user_data: SUserRegister):
role=0, black_phoenix=0)
user = await authenticate_user_by_email(user_data.email, user_data.password)
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}
@ -52,13 +52,13 @@ async def login_user(response: Response, user_data: SUserLogin):
if not user:
raise IncorrectAuthDataException
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}
@router.post("/logout")
async def logout_user(response: Response):
response.delete_cookie("booking_access_token")
response.delete_cookie("black_phoenix_access_token")
@router.get("/me")

View file

@ -8,7 +8,7 @@ class SUserLogin(BaseModel):
password: str
class Config:
orm_mode = True
from_attributes = True
class SUserRegister(BaseModel):
@ -18,5 +18,5 @@ class SUserRegister(BaseModel):
date_of_birth: date
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
pyasn1==0.5.1
pycodestyle==2.11.1
pydantic==1.10.14
pydantic_core==2.14.6
pydantic==2.6.0
pydantic-extra-types==2.5.0
pydantic-settings==2.1.0
pydantic_core==2.16.1
pyflakes==3.2.0
pyright==1.1.347
pytest==7.4.4