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