feat: auth Добавлена поддержка сессий пользователей
Добавлена функциональность управления сессиями пользователей, включая создание сессий при входе, получение списка активных сессий, деактивацию отдельных сессий и деактивацию всех сессий пользователя.
This commit is contained in:
parent
89bfb07a23
commit
dc47e4b003
55
api/app/application/sessions_repository.py
Normal file
55
api/app/application/sessions_repository.py
Normal file
@ -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()
|
||||||
@ -1,10 +1,14 @@
|
|||||||
from fastapi import APIRouter, Depends, Response
|
from fastapi import APIRouter, Depends, Response, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
from app.domain.entities.auth import AuthEntity
|
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.domain.entities.token_entity import TokenEntity
|
||||||
from app.infrastructure.auth_service import AuthService
|
from app.infrastructure.auth_service import AuthService
|
||||||
|
from app.infrastructure.dependencies import get_current_user
|
||||||
|
from app.domain.models.users import User
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -19,10 +23,11 @@ router = APIRouter()
|
|||||||
async def auth_user(
|
async def auth_user(
|
||||||
response: Response,
|
response: Response,
|
||||||
user_data: AuthEntity,
|
user_data: AuthEntity,
|
||||||
|
request: Request,
|
||||||
db: AsyncSession = Depends(get_db)
|
db: AsyncSession = Depends(get_db)
|
||||||
):
|
):
|
||||||
auth_service = AuthService(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(
|
response.set_cookie(
|
||||||
key="users_access_token",
|
key="users_access_token",
|
||||||
@ -32,3 +37,46 @@ async def auth_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return token
|
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"}
|
||||||
|
|||||||
@ -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 ###
|
||||||
16
api/app/domain/entities/responses/session.py
Normal file
16
api/app/domain/entities/responses/session.py
Normal file
@ -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
|
||||||
@ -16,6 +16,7 @@ from app.domain.models.patients import Patient
|
|||||||
from app.domain.models.recipients import Recipient
|
from app.domain.models.recipients import Recipient
|
||||||
from app.domain.models.roles import Role
|
from app.domain.models.roles import Role
|
||||||
from app.domain.models.scheduled_appointments import ScheduledAppointment
|
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.set_contents import SetContent
|
||||||
from app.domain.models.sets import Set
|
from app.domain.models.sets import Set
|
||||||
from app.domain.models.users import User
|
from app.domain.models.users import User
|
||||||
|
|||||||
20
api/app/domain/models/sessions.py
Normal file
20
api/app/domain/models/sessions.py
Normal file
@ -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")
|
||||||
@ -25,6 +25,7 @@ class User(BaseModel):
|
|||||||
appointments = relationship('Appointment', back_populates='doctor')
|
appointments = relationship('Appointment', back_populates='doctor')
|
||||||
mailing = relationship('Mailing', back_populates='user')
|
mailing = relationship('Mailing', back_populates='user')
|
||||||
scheduled_appointments = relationship('ScheduledAppointment', back_populates='doctor')
|
scheduled_appointments = relationship('ScheduledAppointment', back_populates='doctor')
|
||||||
|
sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan")
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return check_password_hash(self.password, password)
|
return check_password_hash(self.password, password)
|
||||||
|
|||||||
@ -1,23 +1,37 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from app.application.users_repository import UsersRepository
|
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
|
from app.settings import get_auth_data
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.users_repository = UsersRepository(db)
|
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)
|
user = await self.users_repository.get_by_login(login)
|
||||||
if user and user.check_password(password):
|
if user and user.check_password(password):
|
||||||
access_token = self.create_access_token({"user_id": user.id})
|
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 {
|
return {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"user_id": user.id
|
"user_id": user.id
|
||||||
@ -25,12 +39,27 @@ class AuthService:
|
|||||||
|
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Неправильный логин или пароль")
|
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
|
@staticmethod
|
||||||
def create_access_token(data: dict) -> str:
|
def create_access_token(data: dict) -> str:
|
||||||
to_encode = data.copy()
|
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})
|
to_encode.update({"exp": expire})
|
||||||
auth_data = get_auth_data()
|
auth_data = get_auth_data()
|
||||||
encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm'])
|
encode_jwt = jwt.encode(to_encode, auth_data['secret_key'], algorithm=auth_data['algorithm'])
|
||||||
|
|
||||||
return encode_jwt
|
return encode_jwt
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from app.database.session import get_db
|
|||||||
from app.domain.models.users import User
|
from app.domain.models.users import User
|
||||||
from app.settings import get_auth_data
|
from app.settings import get_auth_data
|
||||||
from app.application.users_repository import UsersRepository
|
from app.application.users_repository import UsersRepository
|
||||||
|
from app.application.sessions_repository import SessionsRepository
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|
||||||
@ -29,6 +30,10 @@ async def get_current_user(
|
|||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный токен")
|
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)
|
user = await UsersRepository(db).get_by_id_with_role(user_id)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден")
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const AdminRoute = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isUserError) {
|
if (isUserError) {
|
||||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>;
|
return <Navigate to="/login"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@ -3,13 +3,13 @@ import {useSelector} from "react-redux";
|
|||||||
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../Components/Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
|
||||||
const PrivateRoute = () => {
|
const PrivateRoute = () => {
|
||||||
const {user, userData, isLoading} = useSelector((state) => state.auth);
|
const {user, userData, isLoading, error} = useSelector((state) => state.auth);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingIndicator/>;
|
return <LoadingIndicator/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user || !userData || userData.is_blocked) {
|
if (error || !user || !userData || userData.is_blocked) {
|
||||||
return <Navigate to="/login"/>;
|
return <Navigate to="/login"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
useDeleteBackupMutation,
|
useDeleteBackupMutation,
|
||||||
useGetBackupsQuery,
|
useGetBackupsQuery,
|
||||||
useUploadBackupMutation,
|
useUploadBackupMutation,
|
||||||
useRestoreBackupMutation,
|
|
||||||
} from "../../../../../Api/backupsApi.js";
|
} from "../../../../../Api/backupsApi.js";
|
||||||
import {notification} from "antd";
|
import {notification} from "antd";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
|
|||||||
@ -74,7 +74,12 @@ const useLoginPage = () => {
|
|||||||
const response = await loginUser(loginData).unwrap();
|
const response = await loginUser(loginData).unwrap();
|
||||||
const token = response.access_token || response.token;
|
const token = response.access_token || response.token;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Сервер не вернул токен авторизации");
|
notification.error({
|
||||||
|
message: "Ошибка при входе",
|
||||||
|
description: "Не удалось войти. Проверьте логин и пароль.",
|
||||||
|
placement: "topRight",
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem("access_token", token);
|
localStorage.setItem("access_token", token);
|
||||||
dispatch(setUser({token}));
|
dispatch(setUser({token}));
|
||||||
|
|||||||
@ -38,6 +38,9 @@ const authSlice = createSlice({
|
|||||||
setError(state, action) {
|
setError(state, action) {
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
|
state.user = null;
|
||||||
|
state.userData = null;
|
||||||
|
localStorage.removeItem("access_token");
|
||||||
},
|
},
|
||||||
logout(state) {
|
logout(state) {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user