Compare commits

...

2 commits

17 changed files with 181 additions and 140 deletions

View 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)

View 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"

View file

@ -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): # Метод проверяет наличие строки с заданными параметрами

View file

@ -64,6 +64,6 @@ class PasswordsМismatchException(BlackPhoenixException):
detail = "Пароли не совпадают"
class UsersIdIsTheSameException(BlackPhoenixException):
class UserCanNotReadThisChatException(BlackPhoenixException):
status_code = status.HTTP_409_CONFLICT
detail = "Айди юзеров совпадают"
detail = "Юзер не может читать этот чат"

View file

@ -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")

View file

@ -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}')

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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])

View file

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

View file

@ -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()

View file

@ -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"}