From dc47e4b003dc6cc3c7583448b0ee04c79cc1c3a3 Mon Sep 17 00:00:00 2001 From: andrei Date: Thu, 3 Jul 2025 09:16:19 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20auth=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20=D1=81=D0=B5=D1=81=D1=81=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена функциональность управления сессиями пользователей, включая создание сессий при входе, получение списка активных сессий, деактивацию отдельных сессий и деактивацию всех сессий пользователя. --- api/app/application/sessions_repository.py | 55 ++++++++ api/app/controllers/auth_router.py | 52 +++++++- ...013393cef10_0008_добавил_тпблицу_сессий.py | 123 ++++++++++++++++++ api/app/domain/entities/responses/session.py | 16 +++ api/app/domain/models/__init__.py | 1 + api/app/domain/models/sessions.py | 20 +++ api/app/domain/models/users.py | 1 + api/app/infrastructure/auth_service.py | 39 +++++- api/app/infrastructure/dependencies.py | 5 + web-app/src/App/AdminRoute.jsx | 2 +- web-app/src/App/PrivateRoute.jsx | 4 +- .../BackupManageTab/useBackupManageTab.js | 1 - .../Pages/LoginPage/useLoginPage.js | 29 +++-- web-app/src/Redux/Slices/authSlice.js | 17 ++- 14 files changed, 335 insertions(+), 30 deletions(-) create mode 100644 api/app/application/sessions_repository.py create mode 100644 api/app/database/migrations/versions/b013393cef10_0008_добавил_тпблицу_сессий.py create mode 100644 api/app/domain/entities/responses/session.py create mode 100644 api/app/domain/models/sessions.py diff --git a/api/app/application/sessions_repository.py b/api/app/application/sessions_repository.py new file mode 100644 index 0000000..c3a34af --- /dev/null +++ b/api/app/application/sessions_repository.py @@ -0,0 +1,55 @@ +import datetime +from typing import Optional, Sequence + +from sqlalchemy import update, delete +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.domain.models.sessions import Session + + +class SessionsRepository: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, session: Session) -> Session: + self.db.add(session) + await self.db.commit() + await self.db.refresh(session) + return session + + async def get_by_id(self, session_id: int) -> Optional[Session]: + result = await self.db.execute( + select(Session).filter_by(id=session_id) + ) + return result.scalars().all() + + async def get_by_token(self, token: str) -> Optional[Session]: + result = await self.db.execute( + select(Session).filter_by(token=token, is_active=True) + ) + return result.scalars().first() + + async def get_by_user_id(self, user_id: int) -> Sequence[Session]: + result = await self.db.execute( + select(Session).filter_by(user_id=user_id, is_active=True) + ) + return result.scalars().all() + + async def deactivate_session(self, session_id: int) -> None: + await self.db.execute( + update(Session).filter_by(id=session_id).values(is_active=False) + ) + await self.db.commit() + + async def deactivate_all_sessions(self, user_id: int) -> None: + await self.db.execute( + update(Session).filter_by(user_id=user_id).values(is_active=False) + ) + await self.db.commit() + + async def cleanup_expired_sessions(self) -> None: + await self.db.execute( + delete(Session).filter(Session.expires_at < datetime.datetime.now()) + ) + await self.db.commit() diff --git a/api/app/controllers/auth_router.py b/api/app/controllers/auth_router.py index c4c2644..de48ddc 100644 --- a/api/app/controllers/auth_router.py +++ b/api/app/controllers/auth_router.py @@ -1,10 +1,14 @@ -from fastapi import APIRouter, Depends, Response +from fastapi import APIRouter, Depends, Response, Request from sqlalchemy.ext.asyncio import AsyncSession +from typing import List from app.database.session import get_db from app.domain.entities.auth import AuthEntity +from app.domain.entities.responses.session import SessionEntity from app.domain.entities.token_entity import TokenEntity from app.infrastructure.auth_service import AuthService +from app.infrastructure.dependencies import get_current_user +from app.domain.models.users import User router = APIRouter() @@ -19,10 +23,11 @@ router = APIRouter() async def auth_user( response: Response, user_data: AuthEntity, + request: Request, db: AsyncSession = Depends(get_db) ): auth_service = AuthService(db) - token = await auth_service.authenticate_user(user_data.login, user_data.password) + token = await auth_service.authenticate_user(user_data.login, user_data.password, request) response.set_cookie( key="users_access_token", @@ -32,3 +37,46 @@ async def auth_user( ) return token + + +@router.get( + "/sessions/", + response_model=List[SessionEntity], + summary="Get user sessions", + description="Returns a list of active sessions for the current user", +) +async def get_sessions( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + auth_service = AuthService(db) + return await auth_service.get_user_sessions(user.id) + + +@router.post( + "/sessions/{session_id}/logout/", + summary="Log out from a specific session", + description="Deactivates a specific session by ID", +) +async def logout_session( + session_id: int, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + auth_service = AuthService(db) + await auth_service.deactivate_session(session_id, user.id) + return {"message": "Session deactivated"} + + +@router.post( + "/sessions/logout_all/", + summary="Log out from all sessions", + description="Deactivates all sessions for the current user", +) +async def logout_all_sessions( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + auth_service = AuthService(db) + await auth_service.deactivate_all_sessions(user.id) + return {"message": "All sessions deactivated"} diff --git a/api/app/database/migrations/versions/b013393cef10_0008_добавил_тпблицу_сессий.py b/api/app/database/migrations/versions/b013393cef10_0008_добавил_тпблицу_сессий.py new file mode 100644 index 0000000..58a8fee --- /dev/null +++ b/api/app/database/migrations/versions/b013393cef10_0008_добавил_тпблицу_сессий.py @@ -0,0 +1,123 @@ +"""0008 добавил тпблицу сессий + +Revision ID: b013393cef10 +Revises: 4f3877d7a2b1 +Create Date: 2025-07-03 09:05:56.233095 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b013393cef10' +down_revision: Union[str, None] = '4f3877d7a2b1' +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('sessions', + sa.Column('token', sa.String(), nullable=False), + sa.Column('device_info', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token'), + schema='public' + ) + op.drop_constraint('appointment_files_appointment_id_fkey', 'appointment_files', type_='foreignkey') + op.create_foreign_key(None, 'appointment_files', 'appointments', ['appointment_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('appointments_patient_id_fkey', 'appointments', type_='foreignkey') + op.drop_constraint('appointments_doctor_id_fkey', 'appointments', type_='foreignkey') + op.drop_constraint('appointments_type_id_fkey', 'appointments', type_='foreignkey') + op.create_foreign_key(None, 'appointments', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'appointments', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'appointments', 'appointment_types', ['type_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('backups_user_id_fkey', 'backups', type_='foreignkey') + op.create_foreign_key(None, 'backups', 'users', ['user_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('lens_type_id_fkey', 'lens', type_='foreignkey') + op.create_foreign_key(None, 'lens', 'lens_types', ['type_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('lens_issues_doctor_id_fkey', 'lens_issues', type_='foreignkey') + op.drop_constraint('lens_issues_patient_id_fkey', 'lens_issues', type_='foreignkey') + op.drop_constraint('lens_issues_lens_id_fkey', 'lens_issues', type_='foreignkey') + op.create_foreign_key(None, 'lens_issues', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'lens_issues', 'lens', ['lens_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'lens_issues', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('mailing_user_id_fkey', 'mailing', type_='foreignkey') + op.create_foreign_key(None, 'mailing', 'users', ['user_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('mailing_options_option_id_fkey', 'mailing_options', type_='foreignkey') + op.drop_constraint('mailing_options_mailing_id_fkey', 'mailing_options', type_='foreignkey') + op.create_foreign_key(None, 'mailing_options', 'mailing', ['mailing_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'mailing_options', 'mailing_delivery_methods', ['option_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('recipients_mailing_id_fkey', 'recipients', type_='foreignkey') + op.drop_constraint('recipients_patient_id_fkey', 'recipients', type_='foreignkey') + op.create_foreign_key(None, 'recipients', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'recipients', 'mailing', ['mailing_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('scheduled_appointments_type_id_fkey', 'scheduled_appointments', type_='foreignkey') + op.drop_constraint('scheduled_appointments_doctor_id_fkey', 'scheduled_appointments', type_='foreignkey') + op.drop_constraint('scheduled_appointments_patient_id_fkey', 'scheduled_appointments', type_='foreignkey') + op.create_foreign_key(None, 'scheduled_appointments', 'appointment_types', ['type_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'scheduled_appointments', 'patients', ['patient_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'scheduled_appointments', 'users', ['doctor_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('set_contents_type_id_fkey', 'set_contents', type_='foreignkey') + op.drop_constraint('set_contents_set_id_fkey', 'set_contents', type_='foreignkey') + op.create_foreign_key(None, 'set_contents', 'sets', ['set_id'], ['id'], source_schema='public', referent_schema='public') + op.create_foreign_key(None, 'set_contents', 'lens_types', ['type_id'], ['id'], source_schema='public', referent_schema='public') + op.drop_constraint('users_role_id_fkey', 'users', type_='foreignkey') + op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'users', schema='public', type_='foreignkey') + op.create_foreign_key('users_role_id_fkey', 'users', 'roles', ['role_id'], ['id']) + op.drop_constraint(None, 'set_contents', schema='public', type_='foreignkey') + op.drop_constraint(None, 'set_contents', schema='public', type_='foreignkey') + op.create_foreign_key('set_contents_set_id_fkey', 'set_contents', 'sets', ['set_id'], ['id']) + op.create_foreign_key('set_contents_type_id_fkey', 'set_contents', 'lens_types', ['type_id'], ['id']) + op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey') + op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey') + op.drop_constraint(None, 'scheduled_appointments', schema='public', type_='foreignkey') + op.create_foreign_key('scheduled_appointments_patient_id_fkey', 'scheduled_appointments', 'patients', ['patient_id'], ['id']) + op.create_foreign_key('scheduled_appointments_doctor_id_fkey', 'scheduled_appointments', 'users', ['doctor_id'], ['id']) + op.create_foreign_key('scheduled_appointments_type_id_fkey', 'scheduled_appointments', 'appointment_types', ['type_id'], ['id']) + op.drop_constraint(None, 'recipients', schema='public', type_='foreignkey') + op.drop_constraint(None, 'recipients', schema='public', type_='foreignkey') + op.create_foreign_key('recipients_patient_id_fkey', 'recipients', 'patients', ['patient_id'], ['id']) + op.create_foreign_key('recipients_mailing_id_fkey', 'recipients', 'mailing', ['mailing_id'], ['id']) + op.drop_constraint(None, 'mailing_options', schema='public', type_='foreignkey') + op.drop_constraint(None, 'mailing_options', schema='public', type_='foreignkey') + op.create_foreign_key('mailing_options_mailing_id_fkey', 'mailing_options', 'mailing', ['mailing_id'], ['id']) + op.create_foreign_key('mailing_options_option_id_fkey', 'mailing_options', 'mailing_delivery_methods', ['option_id'], ['id']) + op.drop_constraint(None, 'mailing', schema='public', type_='foreignkey') + op.create_foreign_key('mailing_user_id_fkey', 'mailing', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey') + op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey') + op.drop_constraint(None, 'lens_issues', schema='public', type_='foreignkey') + op.create_foreign_key('lens_issues_lens_id_fkey', 'lens_issues', 'lens', ['lens_id'], ['id']) + op.create_foreign_key('lens_issues_patient_id_fkey', 'lens_issues', 'patients', ['patient_id'], ['id']) + op.create_foreign_key('lens_issues_doctor_id_fkey', 'lens_issues', 'users', ['doctor_id'], ['id']) + op.drop_constraint(None, 'lens', schema='public', type_='foreignkey') + op.create_foreign_key('lens_type_id_fkey', 'lens', 'lens_types', ['type_id'], ['id']) + op.drop_constraint(None, 'backups', schema='public', type_='foreignkey') + op.create_foreign_key('backups_user_id_fkey', 'backups', 'users', ['user_id'], ['id']) + op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey') + op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey') + op.drop_constraint(None, 'appointments', schema='public', type_='foreignkey') + op.create_foreign_key('appointments_type_id_fkey', 'appointments', 'appointment_types', ['type_id'], ['id']) + op.create_foreign_key('appointments_doctor_id_fkey', 'appointments', 'users', ['doctor_id'], ['id']) + op.create_foreign_key('appointments_patient_id_fkey', 'appointments', 'patients', ['patient_id'], ['id']) + op.drop_constraint(None, 'appointment_files', schema='public', type_='foreignkey') + op.create_foreign_key('appointment_files_appointment_id_fkey', 'appointment_files', 'appointments', ['appointment_id'], ['id']) + op.drop_table('sessions', schema='public') + # ### end Alembic commands ### diff --git a/api/app/domain/entities/responses/session.py b/api/app/domain/entities/responses/session.py new file mode 100644 index 0000000..ca92a5a --- /dev/null +++ b/api/app/domain/entities/responses/session.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional + + +class SessionEntity(BaseModel): + id: int + user_id: int + token: str + device_info: Optional[str] + created_at: datetime + expires_at: datetime + is_active: bool + + class Config: + from_attributes = True diff --git a/api/app/domain/models/__init__.py b/api/app/domain/models/__init__.py index b58d760..bafdd0f 100644 --- a/api/app/domain/models/__init__.py +++ b/api/app/domain/models/__init__.py @@ -16,6 +16,7 @@ from app.domain.models.patients import Patient from app.domain.models.recipients import Recipient from app.domain.models.roles import Role from app.domain.models.scheduled_appointments import ScheduledAppointment +from app.domain.models.sessions import Session from app.domain.models.set_contents import SetContent from app.domain.models.sets import Set from app.domain.models.users import User diff --git a/api/app/domain/models/sessions.py b/api/app/domain/models/sessions.py new file mode 100644 index 0000000..c470251 --- /dev/null +++ b/api/app/domain/models/sessions.py @@ -0,0 +1,20 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean +from sqlalchemy.orm import relationship +from datetime import datetime +from app.domain.models.base import BaseModel +from app.settings import settings + + +class Session(BaseModel): + __tablename__ = 'sessions' + __table_args__ = {"schema": settings.SCHEMA} + + token = Column(String, nullable=False, unique=True) + device_info = Column(String, nullable=True) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + expires_at = Column(DateTime, nullable=False) + is_active = Column(Boolean, default=True, nullable=False) + + user_id = Column(Integer, ForeignKey(f'{settings.SCHEMA}.users.id'), nullable=False) + + user = relationship("User", back_populates="sessions") diff --git a/api/app/domain/models/users.py b/api/app/domain/models/users.py index b0baf20..81a581e 100644 --- a/api/app/domain/models/users.py +++ b/api/app/domain/models/users.py @@ -25,6 +25,7 @@ class User(BaseModel): appointments = relationship('Appointment', back_populates='doctor') mailing = relationship('Mailing', back_populates='user') scheduled_appointments = relationship('ScheduledAppointment', back_populates='doctor') + sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") def check_password(self, password): return check_password_hash(self.password, password) diff --git a/api/app/infrastructure/auth_service.py b/api/app/infrastructure/auth_service.py index dff7dd8..9be0f83 100644 --- a/api/app/infrastructure/auth_service.py +++ b/api/app/infrastructure/auth_service.py @@ -1,23 +1,37 @@ import datetime -from typing import Optional +from typing import Optional, List import jwt -from fastapi import HTTPException +from fastapi import HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from starlette import status from app.application.users_repository import UsersRepository +from app.application.sessions_repository import SessionsRepository +from app.domain.entities.responses.session import SessionEntity +from app.domain.models.sessions import Session from app.settings import get_auth_data class AuthService: def __init__(self, db: AsyncSession): self.users_repository = UsersRepository(db) + self.sessions_repository = SessionsRepository(db) - async def authenticate_user(self, login: str, password: str) -> Optional[dict]: + async def authenticate_user(self, login: str, password: str, request: Request) -> Optional[dict]: user = await self.users_repository.get_by_login(login) if user and user.check_password(password): access_token = self.create_access_token({"user_id": user.id}) + # Создаем сессию + session = Session( + user_id=user.id, + token=access_token, + device_info=request.headers.get("User-Agent", "Unknown"), + created_at=datetime.datetime.now(), # Naive datetime + expires_at=datetime.datetime.now() + datetime.timedelta(days=30), + is_active=True + ) + await self.sessions_repository.create(session) return { "access_token": access_token, "user_id": user.id @@ -25,12 +39,27 @@ class AuthService: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Неправильный логин или пароль") + async def get_user_sessions(self, user_id: int) -> List[SessionEntity]: + sessions = await self.sessions_repository.get_by_user_id(user_id) + return [SessionEntity.from_orm(session) for session in sessions] + + async def deactivate_session(self, session_id: int, user_id: int) -> None: + session = await self.sessions_repository.get_by_id(session_id) + if not session or session.user_id != user_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Сессия не найдена или доступ запрещен") + await self.sessions_repository.deactivate_session(session_id) + + async def deactivate_all_sessions(self, user_id: int) -> None: + await self.sessions_repository.deactivate_all_sessions(user_id) + + async def cleanup_expired_sessions(self) -> None: + await self.sessions_repository.cleanup_expired_sessions() + @staticmethod def create_access_token(data: dict) -> str: to_encode = data.copy() - expire = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30) + expire = datetime.datetime.now() + datetime.timedelta(days=30) # Naive datetime to_encode.update({"exp": expire}) auth_data = get_auth_data() encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm']) - return encode_jwt diff --git a/api/app/infrastructure/dependencies.py b/api/app/infrastructure/dependencies.py index 45e635b..7b74f02 100644 --- a/api/app/infrastructure/dependencies.py +++ b/api/app/infrastructure/dependencies.py @@ -8,6 +8,7 @@ from app.database.session import get_db from app.domain.models.users import User from app.settings import get_auth_data from app.application.users_repository import UsersRepository +from app.application.sessions_repository import SessionsRepository security = HTTPBearer() @@ -29,6 +30,10 @@ async def get_current_user( if user_id is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен") + session = await SessionsRepository(db).get_by_token(credentials.credentials) + if not session or not session.is_active: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Сессия неактивна или не найдена") + user = await UsersRepository(db).get_by_id_with_role(user_id) if user is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден") diff --git a/web-app/src/App/AdminRoute.jsx b/web-app/src/App/AdminRoute.jsx index 063670f..9af815b 100644 --- a/web-app/src/App/AdminRoute.jsx +++ b/web-app/src/App/AdminRoute.jsx @@ -17,7 +17,7 @@ const AdminRoute = () => { } if (isUserError) { - return ; + return ; } if (!user) { diff --git a/web-app/src/App/PrivateRoute.jsx b/web-app/src/App/PrivateRoute.jsx index f08589e..8ad2760 100644 --- a/web-app/src/App/PrivateRoute.jsx +++ b/web-app/src/App/PrivateRoute.jsx @@ -3,13 +3,13 @@ import {useSelector} from "react-redux"; import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx"; const PrivateRoute = () => { - const {user, userData, isLoading} = useSelector((state) => state.auth); + const {user, userData, isLoading, error} = useSelector((state) => state.auth); if (isLoading) { return ; } - if (!user || !userData || userData.is_blocked) { + if (error || !user || !userData || userData.is_blocked) { return ; } diff --git a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js index f90f768..fd69f98 100644 --- a/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js +++ b/web-app/src/Components/Pages/AdminPage/Components/BackupManageTab/useBackupManageTab.js @@ -3,7 +3,6 @@ import { useDeleteBackupMutation, useGetBackupsQuery, useUploadBackupMutation, - useRestoreBackupMutation, } from "../../../../../Api/backupsApi.js"; import {notification} from "antd"; import {useState} from "react"; diff --git a/web-app/src/Components/Pages/LoginPage/useLoginPage.js b/web-app/src/Components/Pages/LoginPage/useLoginPage.js index cad7cc7..e7e8a09 100644 --- a/web-app/src/Components/Pages/LoginPage/useLoginPage.js +++ b/web-app/src/Components/Pages/LoginPage/useLoginPage.js @@ -1,18 +1,18 @@ -import { useEffect, useRef } from "react"; -import { useNavigate } from "react-router-dom"; -import { useDispatch, useSelector } from "react-redux"; -import { Grid, notification } from "antd"; -import { setError, setUser } from "../../../Redux/Slices/authSlice.js"; -import { useLoginMutation } from "../../../Api/authApi.js"; -import { checkAuth } from "../../../Redux/Slices/authSlice.js"; +import {useEffect, useRef} from "react"; +import {useNavigate} from "react-router-dom"; +import {useDispatch, useSelector} from "react-redux"; +import {Grid, notification} from "antd"; +import {setError, setUser} from "../../../Redux/Slices/authSlice.js"; +import {useLoginMutation} from "../../../Api/authApi.js"; +import {checkAuth} from "../../../Redux/Slices/authSlice.js"; -const { useBreakpoint } = Grid; +const {useBreakpoint} = Grid; const useLoginPage = () => { const navigate = useNavigate(); const dispatch = useDispatch(); - const [loginUser, { isLoading }] = useLoginMutation(); - const { user, userData } = useSelector((state) => state.auth); + const [loginUser, {isLoading}] = useLoginMutation(); + const {user, userData} = useSelector((state) => state.auth); const screens = useBreakpoint(); const hasRedirected = useRef(false); @@ -74,10 +74,15 @@ const useLoginPage = () => { const response = await loginUser(loginData).unwrap(); const token = response.access_token || response.token; if (!token) { - throw new Error("Сервер не вернул токен авторизации"); + notification.error({ + message: "Ошибка при входе", + description: "Не удалось войти. Проверьте логин и пароль.", + placement: "topRight", + }); + return; } localStorage.setItem("access_token", token); - dispatch(setUser({ token })); + dispatch(setUser({token})); await dispatch(checkAuth()).unwrap(); } catch (error) { diff --git a/web-app/src/Redux/Slices/authSlice.js b/web-app/src/Redux/Slices/authSlice.js index 740b671..4d0e10f 100644 --- a/web-app/src/Redux/Slices/authSlice.js +++ b/web-app/src/Redux/Slices/authSlice.js @@ -1,7 +1,7 @@ -import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; -import { usersApi } from "../../Api/usersApi.js"; +import {createSlice, createAsyncThunk} from "@reduxjs/toolkit"; +import {usersApi} from "../../Api/usersApi.js"; -export const checkAuth = createAsyncThunk("auth/checkAuth", async (_, { dispatch, rejectWithValue }) => { +export const checkAuth = createAsyncThunk("auth/checkAuth", async (_, {dispatch, rejectWithValue}) => { try { const token = localStorage.getItem("access_token"); if (!token) { @@ -9,10 +9,10 @@ export const checkAuth = createAsyncThunk("auth/checkAuth", async (_, { dispatch } const userData = await dispatch( - usersApi.endpoints.getAuthenticatedUserData.initiate(undefined, { forceRefetch: true }) + usersApi.endpoints.getAuthenticatedUserData.initiate(undefined, {forceRefetch: true}) ).unwrap(); - return { token, userData }; + return {token, userData}; } catch (error) { localStorage.removeItem("access_token"); return rejectWithValue(error?.data?.detail || "Failed to authenticate"); @@ -38,6 +38,9 @@ const authSlice = createSlice({ setError(state, action) { state.error = action.payload; state.isLoading = false; + state.user = null; + state.userData = null; + localStorage.removeItem("access_token"); }, logout(state) { state.user = null; @@ -57,7 +60,7 @@ const authSlice = createSlice({ state.error = null; }) .addCase(checkAuth.fulfilled, (state, action) => { - state.user = { token: action.payload.token }; + state.user = {token: action.payload.token}; state.userData = action.payload.userData; state.isLoading = false; }) @@ -70,5 +73,5 @@ const authSlice = createSlice({ }, }); -export const { setUser, setError, logout, setUserData } = authSlice.actions; +export const {setUser, setError, logout, setUserData} = authSlice.actions; export default authSlice.reducer; \ No newline at end of file