Compare commits
2 commits
94cd0e36a6
...
28efd76c70
Author | SHA1 | Date | |
---|---|---|---|
28efd76c70 | |||
70653e71e2 |
17 changed files with 181 additions and 140 deletions
40
chat_test/app/admin/auth.py
Normal file
40
chat_test/app/admin/auth.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
from sqladmin.authentication import AuthenticationBackend
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.config import settings
|
||||
from app.users.auth import authenticate_user_by_username, create_access_token, validate_user_admin
|
||||
from app.users.dependencies import get_current_user
|
||||
|
||||
ADMIN_ROLE = 100
|
||||
|
||||
|
||||
class AdminAuth(AuthenticationBackend):
|
||||
async def login(self, request: Request) -> bool:
|
||||
form = await request.form()
|
||||
username, password = form["username"], form["password"]
|
||||
|
||||
user = await authenticate_user_by_username(username, password)
|
||||
if user and user.role == ADMIN_ROLE:
|
||||
access_token = create_access_token({"sub": str(user.id)})
|
||||
request.session.update({"token": access_token})
|
||||
|
||||
return True
|
||||
|
||||
async def logout(self, request: Request) -> bool:
|
||||
# Usually you'd want to just clear the session
|
||||
request.session.clear()
|
||||
return True
|
||||
|
||||
async def authenticate(self, request: Request) -> bool:
|
||||
token = request.session.get("token")
|
||||
|
||||
if not token:
|
||||
return False
|
||||
|
||||
user = await get_current_user(token)
|
||||
if user:
|
||||
return await validate_user_admin(user.id)
|
||||
return False
|
||||
|
||||
|
||||
authentication_backend = AdminAuth(secret_key=settings.SECRET_KEY)
|
54
chat_test/app/admin/views.py
Normal file
54
chat_test/app/admin/views.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from sqladmin import ModelView
|
||||
|
||||
from app.users.models import Users
|
||||
from app.users.chat.models import Chats, UsersXChats, Messages
|
||||
|
||||
|
||||
class UsersAdmin(ModelView, model=Users):
|
||||
column_list = [
|
||||
Users.id,
|
||||
Users.email,
|
||||
Users.username,
|
||||
Users.role,
|
||||
Users.black_phoenix,
|
||||
Users.avatar_image,
|
||||
Users.date_of_birth,
|
||||
Users.date_of_registration,
|
||||
Users.usersxchats,
|
||||
]
|
||||
|
||||
column_details_exclude_list = [Users.hashed_password]
|
||||
can_delete = False
|
||||
name = "Пользователь"
|
||||
name_plural = "Пользователи"
|
||||
icon = "fa-solid fa-users"
|
||||
|
||||
|
||||
class ChatsAdmin(ModelView, model=Chats):
|
||||
column_list = [Chats.id, Chats.chat_for, Chats.usersxchats]
|
||||
name = "Чат"
|
||||
name_plural = "Чаты"
|
||||
icon = "fa-solid fa-comment"
|
||||
|
||||
|
||||
class MessagesAdmin(ModelView, model=Messages):
|
||||
column_list = [
|
||||
Messages.id,
|
||||
Messages.chat_id,
|
||||
Messages.user_id,
|
||||
Messages.message,
|
||||
Messages.image_url,
|
||||
Messages.created_at,
|
||||
Messages.visibility,
|
||||
Messages.user,
|
||||
]
|
||||
name = "Сообщение"
|
||||
name_plural = "Сообщения"
|
||||
icon = "fa-solid fa-sms"
|
||||
|
||||
|
||||
class UsersXChatsAdmin(ModelView, model=UsersXChats):
|
||||
column_list = [UsersXChats.user_id, UsersXChats.chat_id]
|
||||
name = "Допущенный чат"
|
||||
name_plural = "Допущенные чаты"
|
||||
icon = "fa-solid fa-list"
|
|
@ -9,9 +9,10 @@ class BaseDAO:
|
|||
@classmethod
|
||||
async def add(cls, **data): # Метод добавляет данные в БД
|
||||
async with async_session_maker() as session:
|
||||
query = insert(cls.model).values(**data)
|
||||
await session.execute(query)
|
||||
query = insert(cls.model).values(**data).returning(cls.model.id)
|
||||
result = await session.execute(query)
|
||||
await session.commit()
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
async def find_one_or_none(cls, **filter_by): # Метод проверяет наличие строки с заданными параметрами
|
||||
|
|
|
@ -64,6 +64,6 @@ class PasswordsМismatchException(BlackPhoenixException):
|
|||
detail = "Пароли не совпадают"
|
||||
|
||||
|
||||
class UsersIdIsTheSameException(BlackPhoenixException):
|
||||
class UserCanNotReadThisChatException(BlackPhoenixException):
|
||||
status_code = status.HTTP_409_CONFLICT
|
||||
detail = "Айди юзеров совпадают"
|
||||
detail = "Юзер не может читать этот чат"
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from fastapi import FastAPI
|
||||
from sqladmin import Admin
|
||||
from starlette.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.admin.auth import authentication_backend
|
||||
from app.admin.views import UsersAdmin, ChatsAdmin, MessagesAdmin, UsersXChatsAdmin
|
||||
from app.database import engine
|
||||
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
|
||||
|
@ -33,6 +37,13 @@ app.add_middleware(
|
|||
],
|
||||
)
|
||||
|
||||
admin = Admin(app, engine, authentication_backend=authentication_backend)
|
||||
|
||||
admin.add_view(UsersAdmin)
|
||||
admin.add_view(ChatsAdmin)
|
||||
admin.add_view(MessagesAdmin)
|
||||
admin.add_view(UsersXChatsAdmin)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
|
||||
|
||||
|
|
|
@ -8,3 +8,5 @@ celery = Celery(
|
|||
broker=f'redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}',
|
||||
include=['app.tasks.tasks']
|
||||
)
|
||||
|
||||
celery.conf.update(result_backend=f'redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}')
|
||||
|
|
|
@ -32,3 +32,5 @@ def send_registration_confirmation_email(
|
|||
server.login(settings.SMTP_USER, settings.SMTP_PASS)
|
||||
server.send_message(msg_content)
|
||||
|
||||
return confirmation_code
|
||||
|
||||
|
|
|
@ -5,10 +5,14 @@ from passlib.context import CryptContext
|
|||
from pydantic import EmailStr
|
||||
|
||||
from app.config import settings
|
||||
from app.exceptions import UserDontHavePermissionException
|
||||
from app.users.dao import UserDAO
|
||||
from app.users.models import Users
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
ADMIN_ROLE = 100
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
@ -29,16 +33,36 @@ def create_access_token(data: dict) -> str:
|
|||
return encoded_jwt
|
||||
|
||||
|
||||
# Функция проверки наличия юзера
|
||||
async def authenticate_user_by_email(email: EmailStr, password: str):
|
||||
# Функция проверки наличия юзера по мейлу
|
||||
async def authenticate_user_by_email(email: EmailStr, password: str) -> Users | None:
|
||||
user = await UserDAO.find_one_or_none(email=email)
|
||||
if not user or not verify_password(password, user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
async def authenticate_user_by_username(username: str, password: str):
|
||||
# Функция проверки наличия юзера по нику
|
||||
async def authenticate_user_by_username(username: str, password: str) -> Users | None:
|
||||
user = await UserDAO.find_one_or_none(username=username)
|
||||
if not user or not verify_password(password, user.hashed_password):
|
||||
return None
|
||||
return user
|
||||
|
||||
|
||||
async def get_user_allowed_chats(user_id: int):
|
||||
user_allowed_chats = await UserDAO.get_user_allowed_chats(user_id)
|
||||
return user_allowed_chats
|
||||
|
||||
|
||||
async def validate_user_access_to_chat(user_id: int, chat_id: int):
|
||||
user_allowed_chats = await get_user_allowed_chats(user_id=user_id)
|
||||
if not chat_id in user_allowed_chats:
|
||||
raise UserDontHavePermissionException
|
||||
return True
|
||||
|
||||
|
||||
async def validate_user_admin(user_id: int):
|
||||
user_role = await UserDAO.get_user_role(user_id=user_id)
|
||||
if user_role == ADMIN_ROLE:
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -36,7 +36,7 @@ class Messages(Base):
|
|||
user = relationship("Users", back_populates="message")
|
||||
|
||||
def __str__(self):
|
||||
return f"#{self.id} {self.text} от {self.user_id}. Написано {self.created_at}"
|
||||
return f"#{self.id} {self.message} от {self.user_id}. Написано {self.created_at}"
|
||||
|
||||
|
||||
class UsersXChats(Base):
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from fastapi import APIRouter, Depends, status
|
||||
|
||||
from app.exceptions import UserDontHavePermissionException, MessageNotFoundException, UsersIdIsTheSameException
|
||||
from app.exceptions import UserDontHavePermissionException, MessageNotFoundException, UserCanNotReadThisChatException
|
||||
from app.users.chat.dao import ChatDAO
|
||||
from app.users.chat.shemas import SMessage, SLastMessages
|
||||
|
||||
from app.users.dao import UserDAO
|
||||
from app.users.dependencies import get_current_user, validate_user_access_to_chat
|
||||
from app.users.dependencies import get_current_user
|
||||
from app.users.auth import validate_user_access_to_chat, validate_user_admin
|
||||
from app.users.models import Users
|
||||
|
||||
router = APIRouter(
|
||||
|
@ -43,8 +44,7 @@ async def delete_message_from_chat(
|
|||
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:
|
||||
if not await validate_user_admin(user_id=user.id):
|
||||
raise UserDontHavePermissionException
|
||||
deleted_message = await ChatDAO.delete_message(message_id=message_id)
|
||||
return deleted_message
|
||||
|
@ -57,7 +57,7 @@ async def create_chat(
|
|||
user: Users = Depends(get_current_user)
|
||||
):
|
||||
if user.id == user_to_exclude:
|
||||
raise UsersIdIsTheSameException
|
||||
raise UserCanNotReadThisChatException
|
||||
created_chat = await ChatDAO.create(user_id=user_to_exclude)
|
||||
return created_chat
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Dict, List
|
|||
from fastapi import WebSocket, Depends, WebSocketDisconnect
|
||||
|
||||
from app.users.chat.dao import ChatDAO
|
||||
from app.users.dependencies import validate_user_access_to_chat, get_current_user
|
||||
from app.users.auth import validate_user_access_to_chat
|
||||
from app.users.models import Users
|
||||
from app.users.chat.router import router
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from sqlalchemy import update, select
|
||||
from sqlalchemy import update, select, insert
|
||||
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
|
||||
from app.users.models import Users, UsersVerificationCodes
|
||||
|
||||
|
||||
class UserDAO(BaseDAO):
|
||||
|
@ -41,3 +41,21 @@ class UserDAO(BaseDAO):
|
|||
async with async_session_maker() as session:
|
||||
result = await session.execute(query)
|
||||
return result.scalar()
|
||||
|
||||
|
||||
class UserCodesDAO(BaseDAO):
|
||||
model = UsersVerificationCodes
|
||||
|
||||
@classmethod
|
||||
async def set_user_codes(cls, user_id: int, code: str):
|
||||
query = (insert(UsersVerificationCodes)
|
||||
.values(user_id=user_id, code=code, description="Код подтверждения почты")
|
||||
.returning(cls.model.code))
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(query)
|
||||
await session.commit()
|
||||
return result.scalar()
|
||||
|
||||
@classmethod
|
||||
async def get_user_codes(cls, user_id: int):
|
||||
pass
|
||||
|
|
|
@ -6,7 +6,7 @@ from jose import JWTError, jwt
|
|||
from app.config import settings
|
||||
from app.exceptions import (IncorrectTokenFormatException,
|
||||
TokenAbsentException, TokenExpiredException,
|
||||
UserIsNotPresentException, UserDontHavePermissionException)
|
||||
UserIsNotPresentException)
|
||||
from app.users.dao import UserDAO
|
||||
from app.users.models import Users
|
||||
|
||||
|
@ -18,7 +18,7 @@ def get_token(request: Request):
|
|||
return token
|
||||
|
||||
|
||||
async def get_current_user(token: str = Depends(get_token)):
|
||||
async def get_current_user(token: str = Depends(get_token)) -> Users:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, settings.ALGORITHM
|
||||
|
@ -37,13 +37,3 @@ async def get_current_user(token: str = Depends(get_token)):
|
|||
return user
|
||||
|
||||
|
||||
async def get_user_allowed_chats(user_id: int):
|
||||
user_allowed_chats = await UserDAO.get_user_allowed_chats(user_id)
|
||||
return user_allowed_chats
|
||||
|
||||
|
||||
async def validate_user_access_to_chat(user_id: int, chat_id: int):
|
||||
user_allowed_chats = await get_user_allowed_chats(user_id=user_id)
|
||||
if not chat_id in user_allowed_chats:
|
||||
raise UserDontHavePermissionException
|
||||
return True
|
||||
|
|
|
@ -6,7 +6,7 @@ from app.exceptions import UserAlreadyExistsException, IncorrectAuthDataExceptio
|
|||
IncorrectPasswordException, PasswordsМismatchException
|
||||
from app.users.auth import get_password_hash, authenticate_user_by_email, authenticate_user_by_username, \
|
||||
create_access_token, verify_password
|
||||
from app.users.dao import UserDAO
|
||||
from app.users.dao import UserDAO, UserCodesDAO
|
||||
from app.users.dependencies import get_current_user
|
||||
from app.users.models import Users
|
||||
from app.users.schemas import SUserLogin, SUserRegister, SUser, SUserName, SUserPassword
|
||||
|
@ -38,7 +38,7 @@ async def register_user(response: Response, user_data: SUserRegister):
|
|||
if existing_user:
|
||||
raise UserAlreadyExistsException
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
await UserDAO.add(
|
||||
user_id = await UserDAO.add(
|
||||
email=user_data.email,
|
||||
hashed_password=hashed_password,
|
||||
username=user_data.username,
|
||||
|
@ -46,11 +46,15 @@ async def register_user(response: Response, user_data: SUserRegister):
|
|||
role=0,
|
||||
black_phoenix=False
|
||||
)
|
||||
send_registration_confirmation_email.delay(username=user_data.username, email_to=user_data.email)
|
||||
user = await authenticate_user_by_email(user_data.email, user_data.password)
|
||||
access_token = create_access_token({"sub": str(user.id)})
|
||||
response.set_cookie(key="black_phoenix_access_token", value=access_token, httponly=True)
|
||||
return {"access_token": access_token}
|
||||
|
||||
result = send_registration_confirmation_email.delay(username=user_data.username, email_to=user_data.email)
|
||||
result = result.get()
|
||||
|
||||
if await UserCodesDAO.set_user_codes(user_id=user_id, code=result) == result:
|
||||
user = await authenticate_user_by_email(user_data.email, user_data.password)
|
||||
access_token = create_access_token({"sub": str(user.id)})
|
||||
response.set_cookie(key="black_phoenix_access_token", value=access_token, httponly=True)
|
||||
return {"access_token": access_token}
|
||||
|
||||
|
||||
@router.post("/login", response_model=dict[str, str])
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Generic single-database configuration.
|
|
@ -1,78 +0,0 @@
|
|||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# 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 = 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() -> None:
|
||||
"""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")
|
||||
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() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
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()
|
|
@ -1,26 +0,0 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
Loading…
Add table
Reference in a new issue