Merge pull request 'VORKOUT-8' (#13) from VORKOUT-8 into master
Reviewed-on: #13 Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
This commit is contained in:
		
							
								
								
									
										18
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								Makefile
									
									
									
									
									
								
							@@ -12,8 +12,7 @@ services:
 | 
			
		||||
 | 
			
		||||
start-api:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	source .venv/bin/activate && \
 | 
			
		||||
	python -m ${API_APPLICATION_NAME}
 | 
			
		||||
	poetry run python -m ${API_APPLICATION_NAME}
 | 
			
		||||
 | 
			
		||||
start-client:
 | 
			
		||||
	cd client && \
 | 
			
		||||
@@ -21,25 +20,21 @@ start-client:
 | 
			
		||||
 | 
			
		||||
migrate:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	source .venv/bin/activate && \
 | 
			
		||||
	cd $(API_APPLICATION_NAME)/db && \
 | 
			
		||||
	PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args)
 | 
			
		||||
	PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic upgrade $(args)
 | 
			
		||||
 | 
			
		||||
downgrade:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	source .venv/bin/activate && \
 | 
			
		||||
	cd $(API_APPLICATION_NAME)/db && \
 | 
			
		||||
	PYTHONPATH='../..' alembic downgrade -1
 | 
			
		||||
	PYTHONPATH='../..' poetry run alembic downgrade -1
 | 
			
		||||
 | 
			
		||||
revision:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	source .venv/bin/activate && \
 | 
			
		||||
	cd $(API_APPLICATION_NAME)/db && \
 | 
			
		||||
	PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
 | 
			
		||||
	PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic revision --autogenerate
 | 
			
		||||
 | 
			
		||||
venv-api:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	poetry env activate \
 | 
			
		||||
	poetry install
 | 
			
		||||
 | 
			
		||||
venv-client:
 | 
			
		||||
@@ -62,3 +57,8 @@ format-api:
 | 
			
		||||
check-api:
 | 
			
		||||
	cd api && \
 | 
			
		||||
	poetry run ruff format . --check
 | 
			
		||||
 | 
			
		||||
regenerate-openapi-local:
 | 
			
		||||
	cd client \
 | 
			
		||||
	rm src/types/openapi-types.ts \
 | 
			
		||||
	npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								api/api/db/alembic/versions/93106fbe7d83_.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/api/db/alembic/versions/93106fbe7d83_.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
"""empty message
 | 
			
		||||
 | 
			
		||||
Revision ID: 93106fbe7d83
 | 
			
		||||
Revises: f1b06efacec0
 | 
			
		||||
Create Date: 2025-06-26 16:36:02.270706
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy.dialects import mysql
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = '93106fbe7d83'
 | 
			
		||||
down_revision: Union[str, None] = 'f1b06efacec0'
 | 
			
		||||
branch_labels: Union[str, Sequence[str], None] = None
 | 
			
		||||
depends_on: Union[str, Sequence[str], None] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade() -> None:
 | 
			
		||||
    """Upgrade schema."""
 | 
			
		||||
    # ### commands auto generated by Alembic - please adjust! ###
 | 
			
		||||
    op.alter_column('account_keyring', 'key_value',
 | 
			
		||||
               existing_type=mysql.VARCHAR(length=255),
 | 
			
		||||
               type_=sa.String(length=512),
 | 
			
		||||
               existing_nullable=False)
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
    """Downgrade schema."""
 | 
			
		||||
    # ### commands auto generated by Alembic - please adjust! ###
 | 
			
		||||
    op.alter_column('account_keyring', 'key_value',
 | 
			
		||||
               existing_type=sa.String(length=512),
 | 
			
		||||
               type_=mysql.VARCHAR(length=255),
 | 
			
		||||
               existing_nullable=False)
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
@@ -1,19 +1,17 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
import math
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import insert, select, func
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncConnection
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import func, insert, select
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncConnection
 | 
			
		||||
 | 
			
		||||
from api.db.tables.account import account_table
 | 
			
		||||
 | 
			
		||||
from api.schemas.account.account import User
 | 
			
		||||
from api.schemas.endpoints.account import AllUserResponse, all_user_adapter
 | 
			
		||||
from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[User]:
 | 
			
		||||
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]:
 | 
			
		||||
    """
 | 
			
		||||
    Получает список ползовелей заданных значениями  page, limit.
 | 
			
		||||
    """
 | 
			
		||||
@@ -47,31 +45,28 @@ async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Opt
 | 
			
		||||
 | 
			
		||||
    validated_users = all_user_adapter.validate_python(users_data)
 | 
			
		||||
 | 
			
		||||
    return AllUserResponse(users=validated_users, amount_count=total_count, amount_pages=total_pages)
 | 
			
		||||
    return AllUserResponse(
 | 
			
		||||
        users=validated_users,
 | 
			
		||||
        amount_count=total_count,
 | 
			
		||||
        amount_pages=total_pages,
 | 
			
		||||
        current_page=page,
 | 
			
		||||
        limit=limit,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_by_id(connection: AsyncConnection, id: int) -> Optional[User]:
 | 
			
		||||
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
 | 
			
		||||
    """
 | 
			
		||||
    Получает юзера по id.
 | 
			
		||||
    """
 | 
			
		||||
    query = select(account_table).where(account_table.c.id == id)
 | 
			
		||||
    query = select(account_table).where(account_table.c.id == user_id)
 | 
			
		||||
 | 
			
		||||
    user_db_cursor = await connection.execute(query)
 | 
			
		||||
    user_db = user_db_cursor.one_or_none()
 | 
			
		||||
    user = user_db_cursor.mappings().one_or_none()
 | 
			
		||||
 | 
			
		||||
    if not user_db:
 | 
			
		||||
    if not user:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    user_data = {
 | 
			
		||||
        column.name: (
 | 
			
		||||
            getattr(user_db, column.name).name
 | 
			
		||||
            if isinstance(getattr(user_db, column.name), Enum)
 | 
			
		||||
            else getattr(user_db, column.name)
 | 
			
		||||
        )
 | 
			
		||||
        for column in account_table.columns
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return User.model_validate(user_data)
 | 
			
		||||
    return AllUser.model_validate(user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
 | 
			
		||||
@@ -107,7 +102,7 @@ async def update_user_by_id(connection: AsyncConnection, update_values, user) ->
 | 
			
		||||
    await connection.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]:
 | 
			
		||||
async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
 | 
			
		||||
    """
 | 
			
		||||
    Создает нове поле в таблице account_table.
 | 
			
		||||
    """
 | 
			
		||||
@@ -117,14 +112,15 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int)
 | 
			
		||||
        email=user.email,
 | 
			
		||||
        bind_tenant_id=user.bind_tenant_id,
 | 
			
		||||
        role=user.role.value,
 | 
			
		||||
        meta=user.meta,
 | 
			
		||||
        meta=user.meta or {},
 | 
			
		||||
        creator_id=creator_id,
 | 
			
		||||
        created_at=datetime.now(timezone.utc),
 | 
			
		||||
        status=user.status.value,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    await connection.execute(query)
 | 
			
		||||
    res = await connection.execute(query)
 | 
			
		||||
 | 
			
		||||
    await connection.commit()
 | 
			
		||||
    new_user = await get_user_by_id(connection, res.lastrowid)
 | 
			
		||||
 | 
			
		||||
    return user
 | 
			
		||||
    return new_user
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,14 @@ from api.db.tables.account import account_table, account_keyring_table, KeyType,
 | 
			
		||||
 | 
			
		||||
from api.schemas.account.account import User
 | 
			
		||||
from api.schemas.account.account_keyring import AccountKeyring
 | 
			
		||||
from api.schemas.endpoints.account import AllUser
 | 
			
		||||
 | 
			
		||||
from api.utils.key_id_gen import KeyIdGenerator
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
 | 
			
		||||
async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
 | 
			
		||||
    query = (
 | 
			
		||||
        select(account_table, account_keyring_table)
 | 
			
		||||
        .join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
 | 
			
		||||
@@ -45,7 +46,7 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
 | 
			
		||||
        for column in account_keyring_table.columns
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    user = User.model_validate(user_data)
 | 
			
		||||
    user = AllUser.model_validate(user_data)
 | 
			
		||||
    password = AccountKeyring.model_validate(password_data)
 | 
			
		||||
    return user, password
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
from datetime import datetime, timezone
 | 
			
		||||
from datetime import datetime, timedelta, timezone
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import insert, select
 | 
			
		||||
from sqlalchemy import insert, select, update
 | 
			
		||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncConnection
 | 
			
		||||
 | 
			
		||||
from api.db.tables.account import account_keyring_table
 | 
			
		||||
 | 
			
		||||
from api.db.tables.account import account_keyring_table, KeyStatus, KeyType
 | 
			
		||||
from api.schemas.account.account_keyring import AccountKeyring
 | 
			
		||||
from api.utils.hasher import hasher
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
 | 
			
		||||
@@ -67,3 +68,37 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i
 | 
			
		||||
    await connection.commit()
 | 
			
		||||
 | 
			
		||||
    return key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
 | 
			
		||||
    if password is None:
 | 
			
		||||
        password = hasher.generate_password()
 | 
			
		||||
    hashed_password = hasher.hash_data(password)
 | 
			
		||||
    stmt = mysql_insert(account_keyring_table).values(
 | 
			
		||||
        owner_id=owner_id,
 | 
			
		||||
        key_type=KeyType.PASSWORD.value,
 | 
			
		||||
        key_id="PASSWORD",
 | 
			
		||||
        key_value=hashed_password,
 | 
			
		||||
        created_at=datetime.now(timezone.utc),
 | 
			
		||||
        expiry=datetime.now(timezone.utc) + timedelta(days=365),
 | 
			
		||||
        status=KeyStatus.ACTIVE,
 | 
			
		||||
    )
 | 
			
		||||
    stmt.on_duplicate_key_update(key_value=hashed_password)
 | 
			
		||||
    await connection.execute(stmt)
 | 
			
		||||
    await connection.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_password_key(connection: AsyncConnection, owner_id: int, password: str):
 | 
			
		||||
    stmt = select(account_keyring_table).where(account_keyring_table.c.owner_id == owner_id)
 | 
			
		||||
    result = await connection.execute(stmt)
 | 
			
		||||
    keyring = result.one_or_none()
 | 
			
		||||
    if not keyring:
 | 
			
		||||
        await create_password_key(connection, password, owner_id)
 | 
			
		||||
    else:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            update(account_keyring_table)
 | 
			
		||||
            .values(key_value=hasher.hash_data(password), expiry=datetime.now(timezone.utc) + timedelta(days=365))
 | 
			
		||||
            .where(account_keyring_table.c.owner_id == owner_id)
 | 
			
		||||
        )
 | 
			
		||||
    await connection.execute(stmt)
 | 
			
		||||
    await connection.commit()
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ account_keyring_table = Table(
 | 
			
		||||
    Column("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False),
 | 
			
		||||
    Column("key_type", SQLAEnum(KeyType), primary_key=True, nullable=False),
 | 
			
		||||
    Column("key_id", String(40), primary_key=True, default=None),
 | 
			
		||||
    Column("key_value", String(255), nullable=False),
 | 
			
		||||
    Column("key_value", String(512), nullable=False),
 | 
			
		||||
    Column("created_at", DateTime(timezone=True), server_default=func.now()),
 | 
			
		||||
    Column("expiry", DateTime(timezone=True), nullable=True),
 | 
			
		||||
    Column("status", SQLAEnum(KeyStatus), nullable=False),
 | 
			
		||||
 
 | 
			
		||||
@@ -4,29 +4,24 @@ from fastapi import (
 | 
			
		||||
    HTTPException,
 | 
			
		||||
    status,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncConnection
 | 
			
		||||
 | 
			
		||||
from api.db.connection.session import get_connection_dep
 | 
			
		||||
 | 
			
		||||
from api.db.logic.account import (
 | 
			
		||||
    get_user_by_id,
 | 
			
		||||
    update_user_by_id,
 | 
			
		||||
    create_user,
 | 
			
		||||
    get_user_by_login,
 | 
			
		||||
    get_user_accaunt_page,
 | 
			
		||||
    get_user_by_id,
 | 
			
		||||
    get_user_by_login,
 | 
			
		||||
    update_user_by_id,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from api.schemas.account.account import User
 | 
			
		||||
from api.db.logic.keyring import create_password_key, update_password_key
 | 
			
		||||
from api.db.tables.account import AccountStatus
 | 
			
		||||
from api.schemas.account.account import User
 | 
			
		||||
from api.schemas.base import bearer_schema
 | 
			
		||||
from api.schemas.endpoints.account import UserUpdate, AllUserResponse
 | 
			
		||||
from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate
 | 
			
		||||
from api.services.auth import get_current_user
 | 
			
		||||
 | 
			
		||||
from api.services.user_role_validation import db_user_role_validation
 | 
			
		||||
from api.services.update_data_validation import update_user_data_changes
 | 
			
		||||
 | 
			
		||||
from api.services.user_role_validation import db_user_role_validation
 | 
			
		||||
 | 
			
		||||
api_router = APIRouter(
 | 
			
		||||
    prefix="/account",
 | 
			
		||||
@@ -51,9 +46,11 @@ async def get_all_account(
 | 
			
		||||
    return user_list
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
 | 
			
		||||
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
 | 
			
		||||
async def get_account(
 | 
			
		||||
    user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
 | 
			
		||||
    user_id: int,
 | 
			
		||||
    connection: AsyncConnection = Depends(get_connection_dep),
 | 
			
		||||
    current_user=Depends(get_current_user),
 | 
			
		||||
):
 | 
			
		||||
    authorize_user = await db_user_role_validation(connection, current_user)
 | 
			
		||||
 | 
			
		||||
@@ -65,26 +62,27 @@ async def get_account(
 | 
			
		||||
    return user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
 | 
			
		||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser)
 | 
			
		||||
async def create_account(
 | 
			
		||||
    user: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
 | 
			
		||||
    user: UserCreate,
 | 
			
		||||
    connection: AsyncConnection = Depends(get_connection_dep),
 | 
			
		||||
    current_user=Depends(get_current_user),
 | 
			
		||||
):
 | 
			
		||||
    authorize_user = await db_user_role_validation(connection, current_user)
 | 
			
		||||
 | 
			
		||||
    user_validation = await get_user_by_login(connection, user.login)
 | 
			
		||||
 | 
			
		||||
    if user_validation is None:
 | 
			
		||||
        await create_user(connection, user, authorize_user.id)
 | 
			
		||||
        user_new = await get_user_by_login(connection, user.login)
 | 
			
		||||
        return user_new
 | 
			
		||||
 | 
			
		||||
        new_user = await create_user(connection, user, authorize_user.id)
 | 
			
		||||
        await create_password_key(connection, user.password, new_user.id)
 | 
			
		||||
        return new_user
 | 
			
		||||
    else:
 | 
			
		||||
        raise HTTPException(
 | 
			
		||||
            status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
 | 
			
		||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
 | 
			
		||||
async def update_account(
 | 
			
		||||
    user_id: int,
 | 
			
		||||
    user_update: UserUpdate,
 | 
			
		||||
@@ -97,12 +95,15 @@ async def update_account(
 | 
			
		||||
    if user is None:
 | 
			
		||||
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
 | 
			
		||||
 | 
			
		||||
    if user_update.password is not None:
 | 
			
		||||
        await update_password_key(connection, user.id, user_update.password)
 | 
			
		||||
 | 
			
		||||
    update_values = update_user_data_changes(user_update, user)
 | 
			
		||||
 | 
			
		||||
    if update_values is None:
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
    user_update_data = User.model_validate({**user.model_dump(), **update_values})
 | 
			
		||||
    user_update_data = UserUpdate.model_validate({**user.model_dump(), **update_values})
 | 
			
		||||
 | 
			
		||||
    await update_user_by_id(connection, update_values, user)
 | 
			
		||||
 | 
			
		||||
@@ -113,7 +114,9 @@ async def update_account(
 | 
			
		||||
 | 
			
		||||
@api_router.delete("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
 | 
			
		||||
async def delete_account(
 | 
			
		||||
    user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
 | 
			
		||||
    user_id: int,
 | 
			
		||||
    connection: AsyncConnection = Depends(get_connection_dep),
 | 
			
		||||
    current_user=Depends(get_current_user),
 | 
			
		||||
):
 | 
			
		||||
    authorize_user = await db_user_role_validation(connection, current_user)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
from typing import Optional, List
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
from pydantic import EmailStr, Field, TypeAdapter
 | 
			
		||||
 | 
			
		||||
from api.db.tables.account import AccountRole, AccountStatus
 | 
			
		||||
 | 
			
		||||
from api.schemas.base import Base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -12,6 +12,7 @@ class UserUpdate(Base):
 | 
			
		||||
    name: Optional[str] = Field(None, max_length=100)
 | 
			
		||||
    login: Optional[str] = Field(None, max_length=100)
 | 
			
		||||
    email: Optional[EmailStr] = None
 | 
			
		||||
    password: Optional[str] = None
 | 
			
		||||
    bind_tenant_id: Optional[str] = Field(None, max_length=40)
 | 
			
		||||
    role: Optional[AccountRole] = None
 | 
			
		||||
    meta: Optional[dict] = None
 | 
			
		||||
@@ -20,6 +21,17 @@ class UserUpdate(Base):
 | 
			
		||||
    status: Optional[AccountStatus] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserCreate(Base):
 | 
			
		||||
    name: str = Field(max_length=100)
 | 
			
		||||
    login: str = Field(max_length=100)
 | 
			
		||||
    email: Optional[EmailStr] = None
 | 
			
		||||
    password: Optional[str] = None
 | 
			
		||||
    bind_tenant_id: Optional[str] = Field(None, max_length=40)
 | 
			
		||||
    role: AccountRole
 | 
			
		||||
    meta: Optional[dict] = None
 | 
			
		||||
    status: AccountStatus
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllUser(Base):
 | 
			
		||||
    id: int
 | 
			
		||||
    name: str
 | 
			
		||||
@@ -35,6 +47,8 @@ class AllUserResponse(Base):
 | 
			
		||||
    users: List[AllUser]
 | 
			
		||||
    amount_count: int
 | 
			
		||||
    amount_pages: int
 | 
			
		||||
    current_page: int
 | 
			
		||||
    limit: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
all_user_adapter = TypeAdapter(List[AllUser])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,25 @@
 | 
			
		||||
from fastapi import Request, HTTPException
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from fastapi import HTTPException, Request
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncConnection
 | 
			
		||||
 | 
			
		||||
from api.db.logic.auth import get_user
 | 
			
		||||
 | 
			
		||||
# # from backend.schemas.users.token import TokenData
 | 
			
		||||
from api.schemas.account.account import User
 | 
			
		||||
from api.db.tables.account import AccountStatus
 | 
			
		||||
 | 
			
		||||
from api.utils.hasher import Hasher
 | 
			
		||||
from api.schemas.endpoints.account import AllUser
 | 
			
		||||
from api.utils.hasher import hasher
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_current_user(request: Request) -> Optional[User]:
 | 
			
		||||
async def get_current_user(request: Request) -> str | HTTPException:
 | 
			
		||||
    if not hasattr(request.state, "current_user"):
 | 
			
		||||
        return HTTPException(status_code=401, detail="Unauthorized")
 | 
			
		||||
    return request.state.current_user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[User]:
 | 
			
		||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[AllUser]:
 | 
			
		||||
    sql_user, sql_password = await get_user(connection, username)
 | 
			
		||||
 | 
			
		||||
    if not sql_user or sql_user.status != AccountStatus.ACTIVE:
 | 
			
		||||
        return None
 | 
			
		||||
    hasher = Hasher()
 | 
			
		||||
    if not hasher.verify_data(password, sql_password.key_value):
 | 
			
		||||
        return None
 | 
			
		||||
    return sql_user
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
import hashlib
 | 
			
		||||
import secrets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Хешер для работы с паролем.
 | 
			
		||||
 | 
			
		||||
@@ -14,3 +16,10 @@ class Hasher:
 | 
			
		||||
    def verify_data(self, password: str, hashed: str) -> bool:
 | 
			
		||||
        # Проверяет пароль путем сравнения его хеша с сохраненным хешем.
 | 
			
		||||
        return self.hash_data(password) == hashed
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def generate_password() -> str:
 | 
			
		||||
        return secrets.token_urlsafe(20)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
hasher = Hasher()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,23 @@
 | 
			
		||||
import os
 | 
			
		||||
import asyncio
 | 
			
		||||
import hashlib
 | 
			
		||||
import secrets
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from api.db.connection.session import get_connection
 | 
			
		||||
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
 | 
			
		||||
from api.db.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
 | 
			
		||||
from api.utils.hasher import hasher
 | 
			
		||||
from api.utils.key_id_gen import KeyIdGenerator
 | 
			
		||||
 | 
			
		||||
INIT_LOCK_FILE = "../init.lock"
 | 
			
		||||
DEFAULT_LOGIN = "vorkout"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def hash_password(password: str) -> str:
 | 
			
		||||
    return hashlib.sha256(password.encode()).hexdigest()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_password() -> str:
 | 
			
		||||
    return secrets.token_urlsafe(20)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def init():
 | 
			
		||||
    if os.path.exists(INIT_LOCK_FILE):
 | 
			
		||||
        print("Sorry, service is already initialized")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    async with get_connection() as conn:
 | 
			
		||||
        password = generate_password()
 | 
			
		||||
        hashed_password = hash_password(password)
 | 
			
		||||
        password = hasher.generate_password()
 | 
			
		||||
        hashed_password = hasher.hash_data(password)
 | 
			
		||||
 | 
			
		||||
        create_user_query = account_table.insert().values(
 | 
			
		||||
            name=DEFAULT_LOGIN,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
// import { Auth, Tokens } from '../types/auth';
 | 
			
		||||
import axiosRetry from 'axios-retry';
 | 
			
		||||
import { Auth, Tokens } from '@/types/auth';
 | 
			
		||||
import { useAuthStore } from '@/store/authStore';
 | 
			
		||||
import { AuthService } from '@/services/authService';
 | 
			
		||||
import { User } from '@/types/user';
 | 
			
		||||
import { User, UserCreate, UserUpdate } from '@/types/user';
 | 
			
		||||
 | 
			
		||||
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
 | 
			
		||||
  import.meta.env.VITE_APP_API_URL
 | 
			
		||||
@@ -109,6 +108,23 @@ const api = {
 | 
			
		||||
    );
 | 
			
		||||
    return response.data;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async getUserById(userId: number): Promise<User> {
 | 
			
		||||
    const response = await base.get<User>(`/account/${userId}`);
 | 
			
		||||
    return response.data;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async createUser(user: UserCreate): Promise<User> {
 | 
			
		||||
    const response = await base.post<User>('/account', user);
 | 
			
		||||
    return response.data;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async updateUser(userId: number, user: UserUpdate): Promise<User> {
 | 
			
		||||
    const response = await base.put<User>(`/account/${userId}`, user);
 | 
			
		||||
    return response.data;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // keyrings
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default api;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,16 @@ import { Drawer } from 'antd';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { Avatar, Typography } from 'antd';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useUserSelector } from '@/store/userStore';
 | 
			
		||||
 | 
			
		||||
interface ContentDrawerProps {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
  closeDrawer: () => void;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  type: 'create' | 'edit';
 | 
			
		||||
  login?: string;
 | 
			
		||||
  name?: string;
 | 
			
		||||
  email?: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ContentDrawer({
 | 
			
		||||
@@ -15,7 +19,11 @@ export default function ContentDrawer({
 | 
			
		||||
  closeDrawer,
 | 
			
		||||
  children,
 | 
			
		||||
  type,
 | 
			
		||||
  login,
 | 
			
		||||
  name,
 | 
			
		||||
  email,
 | 
			
		||||
}: ContentDrawerProps) {
 | 
			
		||||
  const user = useUserSelector();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [width, setWidth] = useState<number | string>('30%');
 | 
			
		||||
 | 
			
		||||
@@ -30,6 +38,7 @@ export default function ContentDrawer({
 | 
			
		||||
    window.addEventListener('resize', calculateWidths);
 | 
			
		||||
    return () => window.removeEventListener('resize', calculateWidths);
 | 
			
		||||
  }, []);
 | 
			
		||||
  console.log(login, user?.login, login === user?.login);
 | 
			
		||||
 | 
			
		||||
  const editDrawerTitle = (
 | 
			
		||||
    <div
 | 
			
		||||
@@ -59,16 +68,21 @@ export default function ContentDrawer({
 | 
			
		||||
 | 
			
		||||
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
 | 
			
		||||
        <Avatar
 | 
			
		||||
          src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
 | 
			
		||||
          src={
 | 
			
		||||
            login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
 | 
			
		||||
          }
 | 
			
		||||
          size={40}
 | 
			
		||||
          style={{ flexShrink: 0 }}
 | 
			
		||||
        />
 | 
			
		||||
        <div>
 | 
			
		||||
          <Typography.Text strong style={{ display: 'block' }}>
 | 
			
		||||
            Александр Александров
 | 
			
		||||
          <Typography.Text
 | 
			
		||||
            strong
 | 
			
		||||
            style={{ display: 'block', fontSize: '20px' }}
 | 
			
		||||
          >
 | 
			
		||||
            {name} {login === user?.login ? t('you') : ''}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
          <Typography.Text type="secondary" style={{ fontSize: 14 }}>
 | 
			
		||||
            alexandralex@vorkout.ru
 | 
			
		||||
            {email}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
@@ -152,7 +166,7 @@ export default function ContentDrawer({
 | 
			
		||||
      placement="right"
 | 
			
		||||
      open={open}
 | 
			
		||||
      width={width}
 | 
			
		||||
      destroyOnClose={true}
 | 
			
		||||
      destroyOnHidden={true}
 | 
			
		||||
      closable={false}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import { useUserSelector } from '@/store/userStore';
 | 
			
		||||
import { Avatar } from 'antd';
 | 
			
		||||
import Title from 'antd/es/typography/Title';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import ContentDrawer from './ContentDrawer';
 | 
			
		||||
import UserEdit from './UserEdit';
 | 
			
		||||
 | 
			
		||||
interface HeaderProps {
 | 
			
		||||
  title: string;
 | 
			
		||||
@@ -7,6 +11,13 @@ interface HeaderProps {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Header({ title, additionalContent }: HeaderProps) {
 | 
			
		||||
  const [openEdit, setOpenEdit] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const showEditDrawer = () => setOpenEdit(true);
 | 
			
		||||
  const closeEditDrawer = () => {
 | 
			
		||||
    setOpenEdit(false);
 | 
			
		||||
  };
 | 
			
		||||
  const user = useUserSelector();
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
@@ -43,13 +54,24 @@ export default function Header({ title, additionalContent }: HeaderProps) {
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            justifyContent: 'center',
 | 
			
		||||
          }}
 | 
			
		||||
          onClick={showEditDrawer}
 | 
			
		||||
        >
 | 
			
		||||
          <Avatar
 | 
			
		||||
            size={25.77}
 | 
			
		||||
            src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`}
 | 
			
		||||
            src={`https://gamma.heado.ru/go/ava?name=${user?.login}`}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <ContentDrawer
 | 
			
		||||
        login={user?.login}
 | 
			
		||||
        name={user?.name}
 | 
			
		||||
        email={user?.email}
 | 
			
		||||
        open={openEdit}
 | 
			
		||||
        closeDrawer={closeEditDrawer}
 | 
			
		||||
        type="edit"
 | 
			
		||||
      >
 | 
			
		||||
        {user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
 | 
			
		||||
      </ContentDrawer>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { useUserSelector } from '@/store/userStore';
 | 
			
		||||
import { Divider, Menu, Tooltip } from 'antd';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
@@ -13,6 +14,7 @@ export default function SiderMenu({
 | 
			
		||||
  selectedKey,
 | 
			
		||||
  hangleMenuClick,
 | 
			
		||||
}: SiderMenuProps) {
 | 
			
		||||
  const user = useUserSelector();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const collapseStyle = collapsed
 | 
			
		||||
    ? { fontSize: '12px' }
 | 
			
		||||
@@ -74,15 +76,17 @@ export default function SiderMenu({
 | 
			
		||||
      label: t('settings'),
 | 
			
		||||
      className: 'no-expand-icon',
 | 
			
		||||
      children: [
 | 
			
		||||
        {
 | 
			
		||||
          key: '/accounts',
 | 
			
		||||
          label: !collapsed ? (
 | 
			
		||||
            <Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
 | 
			
		||||
          ) : (
 | 
			
		||||
            t('accounts')
 | 
			
		||||
          ),
 | 
			
		||||
          style: collapseStyle,
 | 
			
		||||
        },
 | 
			
		||||
        user && (user.role === 'OWNER' || user.role === 'ADMIN')
 | 
			
		||||
          ? {
 | 
			
		||||
              key: '/accounts',
 | 
			
		||||
              label: !collapsed ? (
 | 
			
		||||
                <Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
 | 
			
		||||
              ) : (
 | 
			
		||||
                t('accounts')
 | 
			
		||||
              ),
 | 
			
		||||
              style: collapseStyle,
 | 
			
		||||
            }
 | 
			
		||||
          : undefined,
 | 
			
		||||
        {
 | 
			
		||||
          key: '/events-list',
 | 
			
		||||
          label: !collapsed ? (
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,19 @@ import {
 | 
			
		||||
  UploadFile,
 | 
			
		||||
  GetProp,
 | 
			
		||||
  UploadProps,
 | 
			
		||||
} from 'antd';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
  message,
 | 
			
		||||
  Spin,
 | 
			
		||||
} from "antd";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { useUserSelector } from "@/store/userStore";
 | 
			
		||||
import { UserCreate as NewUserCreate } from "@/types/user";
 | 
			
		||||
import { UserService } from "@/services/userService";
 | 
			
		||||
import { LoadingOutlined } from "@ant-design/icons";
 | 
			
		||||
 | 
			
		||||
const { Option } = Select;
 | 
			
		||||
 | 
			
		||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
 | 
			
		||||
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
 | 
			
		||||
 | 
			
		||||
const getBase64 = (file: FileType): Promise<string> =>
 | 
			
		||||
  new Promise((resolve, reject) => {
 | 
			
		||||
@@ -24,10 +30,17 @@ const getBase64 = (file: FileType): Promise<string> =>
 | 
			
		||||
    reader.onerror = (error) => reject(error);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export default function UserCreate() {
 | 
			
		||||
interface UserCreateProps {
 | 
			
		||||
  closeDrawer: () => void;
 | 
			
		||||
  getUsers: () => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
 | 
			
		||||
  const user = useUserSelector();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [previewOpen, setPreviewOpen] = useState(false);
 | 
			
		||||
  const [previewImage, setPreviewImage] = useState('');
 | 
			
		||||
  const [previewImage, setPreviewImage] = useState("");
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [fileList, setFileList] = useState<UploadFile[]>([]);
 | 
			
		||||
 | 
			
		||||
@@ -40,40 +53,49 @@ export default function UserCreate() {
 | 
			
		||||
    setPreviewOpen(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
 | 
			
		||||
  const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
 | 
			
		||||
    setFileList(newFileList);
 | 
			
		||||
 | 
			
		||||
  const onFinish = async (values: NewUserCreate) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    await UserService.createUser(values);
 | 
			
		||||
    await getUsers();
 | 
			
		||||
    closeDrawer();
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
    message.info(t("createdAccountMessage"), 4);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const customUploadButton = (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          height: '102px',
 | 
			
		||||
          width: '102px',
 | 
			
		||||
          backgroundColor: '#E2E2E2',
 | 
			
		||||
          borderRadius: '50%',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          justifyContent: 'center',
 | 
			
		||||
          height: "102px",
 | 
			
		||||
          width: "102px",
 | 
			
		||||
          backgroundColor: "#E2E2E2",
 | 
			
		||||
          borderRadius: "50%",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
          marginBottom: 8,
 | 
			
		||||
          marginTop: 30,
 | 
			
		||||
          cursor: 'pointer',
 | 
			
		||||
          cursor: "pointer",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <img
 | 
			
		||||
          src="./icons/drawer/add_photo_alternate.svg"
 | 
			
		||||
          alt="add_photo_alternate"
 | 
			
		||||
          style={{ height: '18px', width: '18px' }}
 | 
			
		||||
          style={{ height: "18px", width: "18px" }}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <span style={{ fontSize: '14px', color: '#8c8c8c' }}>
 | 
			
		||||
        {t('selectPhoto')}
 | 
			
		||||
      <span style={{ fontSize: "14px", color: "#8c8c8c" }}>
 | 
			
		||||
        {t("selectPhoto")}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const photoToUpload = (
 | 
			
		||||
    <div style={{ height: '102px' }}>
 | 
			
		||||
    <div style={{ height: "102px" }}>
 | 
			
		||||
      <Upload
 | 
			
		||||
        listType="picture-circle"
 | 
			
		||||
        fileList={fileList}
 | 
			
		||||
@@ -85,11 +107,11 @@ export default function UserCreate() {
 | 
			
		||||
      </Upload>
 | 
			
		||||
      {previewImage && (
 | 
			
		||||
        <Image
 | 
			
		||||
          wrapperStyle={{ display: 'none' }}
 | 
			
		||||
          wrapperStyle={{ display: "none" }}
 | 
			
		||||
          preview={{
 | 
			
		||||
            visible: previewOpen,
 | 
			
		||||
            onVisibleChange: (visible) => setPreviewOpen(visible),
 | 
			
		||||
            afterOpenChange: (visible) => !visible && setPreviewImage(''),
 | 
			
		||||
            afterOpenChange: (visible) => !visible && setPreviewImage(""),
 | 
			
		||||
          }}
 | 
			
		||||
          src={previewImage}
 | 
			
		||||
        />
 | 
			
		||||
@@ -100,24 +122,24 @@ export default function UserCreate() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        height: '100%',
 | 
			
		||||
        display: "flex",
 | 
			
		||||
        flexDirection: "column",
 | 
			
		||||
        height: "100%",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          justifyContent: 'center',
 | 
			
		||||
          marginBottom: '36px',
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
          justifyContent: "center",
 | 
			
		||||
          marginBottom: "36px",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            flexDirection: 'column',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flexDirection: "column",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {photoToUpload}
 | 
			
		||||
@@ -126,83 +148,85 @@ export default function UserCreate() {
 | 
			
		||||
      <Form
 | 
			
		||||
        name="user-edit-form"
 | 
			
		||||
        layout="vertical"
 | 
			
		||||
        // onFinish={onFinish}
 | 
			
		||||
        onFinish={onFinish}
 | 
			
		||||
        initialValues={{
 | 
			
		||||
          name: '',
 | 
			
		||||
          login: '',
 | 
			
		||||
          password: '',
 | 
			
		||||
          email: '',
 | 
			
		||||
          tenant: '',
 | 
			
		||||
          role: '',
 | 
			
		||||
          status: '',
 | 
			
		||||
          name: "",
 | 
			
		||||
          login: "",
 | 
			
		||||
          password: "",
 | 
			
		||||
          email: "",
 | 
			
		||||
          bindTenantId: "",
 | 
			
		||||
          role: "",
 | 
			
		||||
          status: "",
 | 
			
		||||
        }}
 | 
			
		||||
        style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
 | 
			
		||||
        style={{ flex: 1, display: "flex", flexDirection: "column" }}
 | 
			
		||||
      >
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('name')}
 | 
			
		||||
          label={t("name")}
 | 
			
		||||
          name="name"
 | 
			
		||||
          rules={[{ required: true, message: t('nameMessage') }]}
 | 
			
		||||
          rules={[{ required: true, message: t("nameMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('login')}
 | 
			
		||||
          label={t("login")}
 | 
			
		||||
          name="login"
 | 
			
		||||
          rules={[{ required: true, message: t('loginMessage') }]}
 | 
			
		||||
          rules={[{ required: true, message: t("loginMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('password')}
 | 
			
		||||
          label={t("password")}
 | 
			
		||||
          name="password"
 | 
			
		||||
          rules={[{ required: true, message: t('passwordMessage') }]}
 | 
			
		||||
          rules={[{ message: t("passwordMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input.Password />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('email')}
 | 
			
		||||
          label={t("email")}
 | 
			
		||||
          name="email"
 | 
			
		||||
          rules={[
 | 
			
		||||
            { required: true, message: t('emailMessage') },
 | 
			
		||||
            { type: 'email', message: t('emailErrorMessage') },
 | 
			
		||||
            { message: t("emailMessage") },
 | 
			
		||||
            { type: "email", message: t("emailErrorMessage") },
 | 
			
		||||
          ]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('tenant')}
 | 
			
		||||
          name="tenant"
 | 
			
		||||
          rules={[{ required: true, message: t('tenantMessage') }]}
 | 
			
		||||
          label={t("tenant")}
 | 
			
		||||
          name="bindTenantId"
 | 
			
		||||
          rules={[{ message: t("tenantMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('role')}
 | 
			
		||||
          label={t("role")}
 | 
			
		||||
          name="role"
 | 
			
		||||
          rules={[{ required: true, message: t('roleMessage') }]}
 | 
			
		||||
          rules={[{ required: true, message: t("roleMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Select placeholder={t('roleMessage')}>
 | 
			
		||||
            <Option value="Директор магазина">Директор магазина</Option>
 | 
			
		||||
            <Option value="Менеджер">Менеджер</Option>
 | 
			
		||||
            <Option value="Кассир">Кассир</Option>
 | 
			
		||||
          <Select placeholder={t("roleMessage")}>
 | 
			
		||||
            {user && user.role === "OWNER" ? (
 | 
			
		||||
              <Option value="ADMIN">{t("ADMIN")}</Option>
 | 
			
		||||
            ) : undefined}
 | 
			
		||||
            <Option value="EDITOR">{t("EDITOR")}</Option>
 | 
			
		||||
            <Option value="VIEWER">{t("VIEWER")}</Option>
 | 
			
		||||
          </Select>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('status')}
 | 
			
		||||
          label={t("status")}
 | 
			
		||||
          name="status"
 | 
			
		||||
          rules={[{ required: true, message: t('statusMessage') }]}
 | 
			
		||||
          rules={[{ required: true, message: t("statusMessage") }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Select placeholder={t('statusMessage')}>
 | 
			
		||||
            <Option value="ACTIVE">Активен</Option>
 | 
			
		||||
            <Option value="DISABLED">Неактивен</Option>
 | 
			
		||||
            <Option value="BLOCKED">Заблокирован</Option>
 | 
			
		||||
            <Option value="DELETED">Удален</Option>
 | 
			
		||||
          <Select placeholder={t("statusMessage")}>
 | 
			
		||||
            <Option value="ACTIVE">{t("ACTIVE")}</Option>
 | 
			
		||||
            <Option value="DISABLED">{t("DISABLED")}</Option>
 | 
			
		||||
            <Option value="BLOCKED">{t("BLOCKED")}</Option>
 | 
			
		||||
            <Option value="DELETED">{t("DELETED")}</Option>
 | 
			
		||||
          </Select>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
@@ -213,14 +237,23 @@ export default function UserCreate() {
 | 
			
		||||
            type="primary"
 | 
			
		||||
            htmlType="submit"
 | 
			
		||||
            block
 | 
			
		||||
            style={{ color: '#000' }}
 | 
			
		||||
            style={{ color: "#000" }}
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              src="/icons/drawer/reg.svg"
 | 
			
		||||
              alt="save"
 | 
			
		||||
              style={{ height: '18px', width: '18px' }}
 | 
			
		||||
            />{' '}
 | 
			
		||||
            {t('addAccount')}
 | 
			
		||||
            {loading ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "}
 | 
			
		||||
                {t("saving")}
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <img
 | 
			
		||||
                  src="/icons/drawer/reg.svg"
 | 
			
		||||
                  alt="save"
 | 
			
		||||
                  style={{ height: "18px", width: "18px" }}
 | 
			
		||||
                />{" "}
 | 
			
		||||
                {t("addAccount")}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
      </Form>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +1,101 @@
 | 
			
		||||
import { Button, Form, Input, Select } from 'antd';
 | 
			
		||||
import { UserService } from '@/services/userService';
 | 
			
		||||
import { useUserSelector } from '@/store/userStore';
 | 
			
		||||
import { UserUpdate } from '@/types/user';
 | 
			
		||||
import { LoadingOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Button, Form, Input, message, Select, Spin } from 'antd';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
const { Option } = Select;
 | 
			
		||||
 | 
			
		||||
export default function UserEdit() {
 | 
			
		||||
interface UserEditProps {
 | 
			
		||||
  userId?: number;
 | 
			
		||||
  closeDrawer: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
 | 
			
		||||
  const currentUser = useUserSelector();
 | 
			
		||||
  const [form] = Form.useForm();
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [user, setUser] = useState<UserUpdate>({
 | 
			
		||||
    id: 0,
 | 
			
		||||
    name: '',
 | 
			
		||||
    login: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    password: '',
 | 
			
		||||
    bindTenantId: '',
 | 
			
		||||
    role: 'VIEWER',
 | 
			
		||||
    meta: {},
 | 
			
		||||
    createdAt: '',
 | 
			
		||||
    status: 'ACTIVE',
 | 
			
		||||
  });
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    async function getUser() {
 | 
			
		||||
      if (typeof userId === 'undefined') {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const user = await UserService.getUserById(userId);
 | 
			
		||||
      setUser(user);
 | 
			
		||||
      form.setFieldsValue({ ...user });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getUser();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const onFinish = async (values: UserUpdate) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    let updatedUser: Partial<UserUpdate> = {};
 | 
			
		||||
 | 
			
		||||
    (Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
 | 
			
		||||
      if (values[key] !== user[key]) {
 | 
			
		||||
        updatedUser = { ...updatedUser, [key]: values[key] };
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (Object.keys(updatedUser).length > 0) {
 | 
			
		||||
      console.log('updateUser', userId, updatedUser);
 | 
			
		||||
      await UserService.updateUser(userId!, updatedUser);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
    message.info(t('editAccountMessage'), 4);
 | 
			
		||||
    closeDrawer();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
 | 
			
		||||
      <Form
 | 
			
		||||
        form={form}
 | 
			
		||||
        name="user-edit-form"
 | 
			
		||||
        layout="vertical"
 | 
			
		||||
        // onFinish={onFinish}
 | 
			
		||||
        initialValues={{
 | 
			
		||||
          name: 'Александр Александров',
 | 
			
		||||
          login: 'alexandralex@vorkout.ru',
 | 
			
		||||
          password: 'jKUUl776GHd',
 | 
			
		||||
          email: 'alexandralex@vorkout.ru',
 | 
			
		||||
          tenant: 'text',
 | 
			
		||||
          role: 'Директор магазина',
 | 
			
		||||
          status: 'Активен',
 | 
			
		||||
        }}
 | 
			
		||||
        onFinish={onFinish}
 | 
			
		||||
        initialValues={{ ...user }}
 | 
			
		||||
        style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
 | 
			
		||||
      >
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('name')}
 | 
			
		||||
          name="name"
 | 
			
		||||
          rules={[{ required: true, message: t('nameMessage') }]}
 | 
			
		||||
          rules={[{ message: t('nameMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('login')}
 | 
			
		||||
          name="login"
 | 
			
		||||
          rules={[{ required: true, message: t('loginMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        {user?.id === currentUser?.id ? undefined : (
 | 
			
		||||
          <Form.Item
 | 
			
		||||
            label={t('login')}
 | 
			
		||||
            name="login"
 | 
			
		||||
            rules={[{ message: t('loginMessage') }]}
 | 
			
		||||
          >
 | 
			
		||||
            <Input />
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('password')}
 | 
			
		||||
          name="password"
 | 
			
		||||
          rules={[{ required: true, message: t('passwordMessage') }]}
 | 
			
		||||
          rules={[{ message: t('passwordMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input.Password />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
@@ -59,23 +113,27 @@ export default function UserEdit() {
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('tenant')}
 | 
			
		||||
          name="tenant"
 | 
			
		||||
          name="bindTenantId"
 | 
			
		||||
          rules={[{ required: true, message: t('tenantMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Input />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('role')}
 | 
			
		||||
          name="role"
 | 
			
		||||
          rules={[{ required: true, message: t('roleMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Select placeholder={t('roleMessage')}>
 | 
			
		||||
            <Option value="Директор магазина">Директор магазина</Option>
 | 
			
		||||
            <Option value="Менеджер">Менеджер</Option>
 | 
			
		||||
            <Option value="Кассир">Кассир</Option>
 | 
			
		||||
          </Select>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        {user?.id === currentUser?.id ? undefined : (
 | 
			
		||||
          <Form.Item
 | 
			
		||||
            label={t('role')}
 | 
			
		||||
            name="role"
 | 
			
		||||
            rules={[{ required: true, message: t('roleMessage') }]}
 | 
			
		||||
          >
 | 
			
		||||
            <Select placeholder={t('roleMessage')}>
 | 
			
		||||
              {currentUser && currentUser.role === 'OWNER' ? (
 | 
			
		||||
                <Option value="ADMIN">{t('ADMIN')}</Option>
 | 
			
		||||
              ) : undefined}
 | 
			
		||||
              <Option value="EDITOR">{t('EDITOR')}</Option>
 | 
			
		||||
              <Option value="VIEWER">{t('VIEWER')}</Option>
 | 
			
		||||
            </Select>
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <Form.Item
 | 
			
		||||
          label={t('status')}
 | 
			
		||||
@@ -83,10 +141,10 @@ export default function UserEdit() {
 | 
			
		||||
          rules={[{ required: true, message: t('statusMessage') }]}
 | 
			
		||||
        >
 | 
			
		||||
          <Select placeholder={t('statusMessage')}>
 | 
			
		||||
            <Option value="ACTIVE">Активен</Option>
 | 
			
		||||
            <Option value="DISABLED">Неактивен</Option>
 | 
			
		||||
            <Option value="BLOCKED">Заблокирован</Option>
 | 
			
		||||
            <Option value="DELETED">Удален</Option>
 | 
			
		||||
            <Option value="ACTIVE">{t('ACTIVE')}</Option>
 | 
			
		||||
            <Option value="DISABLED">{t('DISABLED')}</Option>
 | 
			
		||||
            <Option value="BLOCKED">{t('BLOCKED')}</Option>
 | 
			
		||||
            <Option value="DELETED">{t('DELETED')}</Option>
 | 
			
		||||
          </Select>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
@@ -99,12 +157,21 @@ export default function UserEdit() {
 | 
			
		||||
            block
 | 
			
		||||
            style={{ color: '#000' }}
 | 
			
		||||
          >
 | 
			
		||||
            <img
 | 
			
		||||
              src="/icons/drawer/save.svg"
 | 
			
		||||
              alt="save"
 | 
			
		||||
              style={{ height: '18px', width: '18px' }}
 | 
			
		||||
            />{' '}
 | 
			
		||||
            {t('save')}
 | 
			
		||||
            {loading ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '}
 | 
			
		||||
                {t('saving')}
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <img
 | 
			
		||||
                  src="/icons/drawer/save.svg"
 | 
			
		||||
                  alt="save"
 | 
			
		||||
                  style={{ height: '18px', width: '18px' }}
 | 
			
		||||
                />{' '}
 | 
			
		||||
                {t('save')}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
      </Form>
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,20 @@ i18n
 | 
			
		||||
          addAccount: 'Add account',
 | 
			
		||||
          save: 'Save changes',
 | 
			
		||||
          newAccount: 'New account',
 | 
			
		||||
          ACTIVE: 'Active',
 | 
			
		||||
          DISABLED: 'Disabled',
 | 
			
		||||
          BLOCKED: 'Blocked',
 | 
			
		||||
          DELETED: 'Deleted',
 | 
			
		||||
          OWNER: 'Owner',
 | 
			
		||||
          ADMIN: 'Admin',
 | 
			
		||||
          EDITOR: 'Editor',
 | 
			
		||||
          VIEWER: 'Viewer',
 | 
			
		||||
          nameLogin: 'Name, login',
 | 
			
		||||
          createdAt: 'Created',
 | 
			
		||||
          saving: 'Saving...',
 | 
			
		||||
          createdAccountMessage: 'User successfully created!',
 | 
			
		||||
          editAccountMessage: 'User successfully updated!',
 | 
			
		||||
          you: '(You)',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      ru: {
 | 
			
		||||
@@ -66,6 +80,20 @@ i18n
 | 
			
		||||
          addAccount: 'Добавить аккаунт',
 | 
			
		||||
          save: 'Сохранить изменения',
 | 
			
		||||
          newAccount: 'Новая учетная запись',
 | 
			
		||||
          ACTIVE: 'Активен',
 | 
			
		||||
          DISABLED: 'Выключен',
 | 
			
		||||
          BLOCKED: 'Заблокирован',
 | 
			
		||||
          DELETED: 'Удален',
 | 
			
		||||
          OWNER: 'Владелец',
 | 
			
		||||
          ADMIN: 'Админ',
 | 
			
		||||
          EDITOR: 'Редактор',
 | 
			
		||||
          VIEWER: 'Наблюдатель',
 | 
			
		||||
          nameLogin: 'Имя, Логин',
 | 
			
		||||
          createdAt: 'Создано',
 | 
			
		||||
          saving: 'Сохранение...',
 | 
			
		||||
          createdAccountMessage: 'Пользователь успешно создан!',
 | 
			
		||||
          editAccountMessage: 'Пользователь успешно обновлен!',
 | 
			
		||||
          you: '(Вы)',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +1,219 @@
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { User } from '@/types/user';
 | 
			
		||||
import Header from '@/components/Header';
 | 
			
		||||
import ContentDrawer from '@/components/ContentDrawer';
 | 
			
		||||
import UserCreate from '@/components/UserCreate';
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
 | 
			
		||||
import Header from "@/components/Header";
 | 
			
		||||
import ContentDrawer from "@/components/ContentDrawer";
 | 
			
		||||
import UserCreate from "@/components/UserCreate";
 | 
			
		||||
import { Avatar, Table } from "antd";
 | 
			
		||||
import { TableProps } from "antd/lib";
 | 
			
		||||
import { UserService } from "@/services/userService";
 | 
			
		||||
import UserEdit from "@/components/UserEdit";
 | 
			
		||||
import { useSearchParams } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export default function AccountsPage() {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  const [openCreate, setOpenCreate] = useState(false);
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
 | 
			
		||||
  const showDrawer = () => setOpen(true);
 | 
			
		||||
  const closeDrawer = () => setOpen(false);
 | 
			
		||||
  const [activeAccount, setActiveAccount] = useState<
 | 
			
		||||
    { login: string; id: number; name: string; email: string } | undefined
 | 
			
		||||
  >(undefined);
 | 
			
		||||
 | 
			
		||||
  const [accounts, setAccounts] = useState<User[]>([]);
 | 
			
		||||
  const showCreateDrawer = () => setOpenCreate(true);
 | 
			
		||||
  const closeCreateDrawer = () => {
 | 
			
		||||
    setActiveAccount(undefined);
 | 
			
		||||
    setOpenCreate(false);
 | 
			
		||||
  };
 | 
			
		||||
  const [openEdit, setOpenEdit] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const showEditDrawer = () => setOpenEdit(true);
 | 
			
		||||
  const closeEditDrawer = () => {
 | 
			
		||||
    setActiveAccount(undefined);
 | 
			
		||||
    setOpenEdit(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [accounts, setAccounts] = useState<AllUserResponse>({
 | 
			
		||||
    amountCount: 0,
 | 
			
		||||
    amountPages: 0,
 | 
			
		||||
    users: [],
 | 
			
		||||
    currentPage: 1,
 | 
			
		||||
    limit: 10,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  async function getUsers() {
 | 
			
		||||
    const page = Number(searchParams.get("page") || "1");
 | 
			
		||||
    const limit = Number(searchParams.get("limit") || "10");
 | 
			
		||||
    setSearchParams({
 | 
			
		||||
      page: page.toString(),
 | 
			
		||||
      limit: limit.toString(),
 | 
			
		||||
    });
 | 
			
		||||
    const data = await UserService.getUsers(page, limit);
 | 
			
		||||
    console.log("searchParams", searchParams);
 | 
			
		||||
    setAccounts(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getUsers();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const statusColor = {
 | 
			
		||||
    ACTIVE: "#27AE60",
 | 
			
		||||
    DISABLED: "#606060",
 | 
			
		||||
    BLOCKED: "#FF0000",
 | 
			
		||||
    DELETED: "#B30000",
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const columns: TableProps<AllUser>["columns"] = [
 | 
			
		||||
    {
 | 
			
		||||
      title: "#",
 | 
			
		||||
      dataIndex: "id",
 | 
			
		||||
      key: "id",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: t("nameLogin"),
 | 
			
		||||
      dataIndex: "nameLogin",
 | 
			
		||||
      key: "nameLogin",
 | 
			
		||||
      render: (text, record) => (
 | 
			
		||||
        <div
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setActiveAccount({
 | 
			
		||||
              login: record.login,
 | 
			
		||||
              id: record.id,
 | 
			
		||||
              name: record.name,
 | 
			
		||||
              email: record.email || "",
 | 
			
		||||
            });
 | 
			
		||||
            showEditDrawer();
 | 
			
		||||
          }}
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            alignItems: "center",
 | 
			
		||||
            gap: "16px",
 | 
			
		||||
            cursor: "pointer",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              height: "32px",
 | 
			
		||||
              width: "32px",
 | 
			
		||||
              display: "flex",
 | 
			
		||||
              alignItems: "center",
 | 
			
		||||
              justifyContent: "center",
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Avatar
 | 
			
		||||
              size={32}
 | 
			
		||||
              src={`https://gamma.heado.ru/go/ava?name=${record.login}`}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div style={{ display: "flex", flexDirection: "column" }}>
 | 
			
		||||
            <div>{record.name}</div>
 | 
			
		||||
            <div style={{ color: "#606060" }}>{record.login}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: "E-mail",
 | 
			
		||||
      dataIndex: "email",
 | 
			
		||||
      key: "email",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: t("tenant"),
 | 
			
		||||
      dataIndex: "bindTenantId",
 | 
			
		||||
      key: "tenant",
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: t("role"),
 | 
			
		||||
      dataIndex: "role",
 | 
			
		||||
      key: "role",
 | 
			
		||||
      render: (text) => <div>{t(text)}</div>,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: t("createdAt"),
 | 
			
		||||
      dataIndex: "createdAt",
 | 
			
		||||
      key: "createdAt",
 | 
			
		||||
      render: (text) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          {new Date(text).toLocaleString("ru", {
 | 
			
		||||
            year: "2-digit",
 | 
			
		||||
            month: "2-digit",
 | 
			
		||||
            day: "2-digit",
 | 
			
		||||
          })}
 | 
			
		||||
        </div>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: t("status"),
 | 
			
		||||
      dataIndex: "status",
 | 
			
		||||
      key: "status",
 | 
			
		||||
      render: (text) => (
 | 
			
		||||
        <div style={{ color: statusColor[text as AccountStatus] }}>
 | 
			
		||||
          {t(text)}
 | 
			
		||||
        </div>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const onTableChange: TableProps<AllUser>["onChange"] = (pagination) => {
 | 
			
		||||
    console.log(pagination);
 | 
			
		||||
    UserService.getUsers(
 | 
			
		||||
      pagination.current as number,
 | 
			
		||||
      pagination.pageSize
 | 
			
		||||
    ).then((data) => {
 | 
			
		||||
      setAccounts(data);
 | 
			
		||||
      setSearchParams({
 | 
			
		||||
        page: data.currentPage.toString(),
 | 
			
		||||
        limit: data.limit.toString(),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Header
 | 
			
		||||
        title={t('accounts')}
 | 
			
		||||
        title={t("accounts")}
 | 
			
		||||
        additionalContent={
 | 
			
		||||
          <img
 | 
			
		||||
            src="./icons/header/add_2.svg"
 | 
			
		||||
            alt="add"
 | 
			
		||||
            style={{
 | 
			
		||||
              height: '18px',
 | 
			
		||||
              width: '18px',
 | 
			
		||||
              cursor: 'pointer',
 | 
			
		||||
              height: "18px",
 | 
			
		||||
              width: "18px",
 | 
			
		||||
              cursor: "pointer",
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={showDrawer}
 | 
			
		||||
            onClick={showCreateDrawer}
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Table
 | 
			
		||||
        size="small"
 | 
			
		||||
        onChange={onTableChange}
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        dataSource={accounts.users}
 | 
			
		||||
        pagination={{
 | 
			
		||||
          pageSize: accounts.limit,
 | 
			
		||||
          current: accounts.currentPage,
 | 
			
		||||
          total: accounts.amountCount,
 | 
			
		||||
        }}
 | 
			
		||||
        rowKey={"id"}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <ContentDrawer open={open} closeDrawer={closeDrawer} type="create">
 | 
			
		||||
        <UserCreate />
 | 
			
		||||
      <ContentDrawer
 | 
			
		||||
        open={openCreate}
 | 
			
		||||
        closeDrawer={closeCreateDrawer}
 | 
			
		||||
        type="create"
 | 
			
		||||
      >
 | 
			
		||||
        <UserCreate getUsers={getUsers} closeDrawer={closeCreateDrawer} />
 | 
			
		||||
      </ContentDrawer>
 | 
			
		||||
      <ContentDrawer
 | 
			
		||||
        login={activeAccount?.login}
 | 
			
		||||
        name={activeAccount?.name}
 | 
			
		||||
        email={activeAccount?.email}
 | 
			
		||||
        open={openEdit}
 | 
			
		||||
        closeDrawer={closeEditDrawer}
 | 
			
		||||
        type="edit"
 | 
			
		||||
      >
 | 
			
		||||
        <UserEdit userId={activeAccount?.id} closeDrawer={closeEditDrawer} />
 | 
			
		||||
      </ContentDrawer>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Form, Input, Button, Typography } from 'antd';
 | 
			
		||||
import { Form, Input, Button, Typography, message } from 'antd';
 | 
			
		||||
import {
 | 
			
		||||
  EyeInvisibleOutlined,
 | 
			
		||||
  EyeTwoTone,
 | 
			
		||||
@@ -45,7 +45,11 @@ export default function LoginPage() {
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <Form name="login" onFinish={onFinish} layout="vertical">
 | 
			
		||||
        <Form
 | 
			
		||||
          name="login"
 | 
			
		||||
          onFinish={onFinish}
 | 
			
		||||
          layout="vertical"
 | 
			
		||||
        >
 | 
			
		||||
          <Form.Item
 | 
			
		||||
            name="login"
 | 
			
		||||
            rules={[{ required: true, message: 'Введите login' }]}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import api from '@/api/api';
 | 
			
		||||
import { User } from '@/types/user';
 | 
			
		||||
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
 | 
			
		||||
 | 
			
		||||
export class UserService {
 | 
			
		||||
  static async getProfile(): Promise<User> {
 | 
			
		||||
@@ -9,8 +9,30 @@ export class UserService {
 | 
			
		||||
    return user;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
 | 
			
		||||
    const users = api.getUsers(page, limit);
 | 
			
		||||
    return users;
 | 
			
		||||
  static async getUsers(
 | 
			
		||||
    page: number = 1,
 | 
			
		||||
    limit: number = 10
 | 
			
		||||
  ): Promise<AllUserResponse> {
 | 
			
		||||
    console.log('getUsers');
 | 
			
		||||
    const allUsers = api.getUsers(page, limit);
 | 
			
		||||
    return allUsers;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async getUserById(userId: number): Promise<User> {
 | 
			
		||||
    console.log('getUserById');
 | 
			
		||||
    const user = api.getUserById(userId);
 | 
			
		||||
    return user;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async createUser(user: UserCreate): Promise<User> {
 | 
			
		||||
    console.log('createUser');
 | 
			
		||||
    const createdUser = api.createUser(user);
 | 
			
		||||
    return createdUser;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static async updateUser(userId: number, user: UserUpdate): Promise<User> {
 | 
			
		||||
    console.log('updateUser');
 | 
			
		||||
    const updatedUser = api.updateUser(userId, user);
 | 
			
		||||
    return updatedUser;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -191,6 +191,10 @@ export interface components {
 | 
			
		||||
            amountCount: number;
 | 
			
		||||
            /** Amountpages */
 | 
			
		||||
            amountPages: number;
 | 
			
		||||
            /** Currentpage */
 | 
			
		||||
            currentPage: number;
 | 
			
		||||
            /** Limit */
 | 
			
		||||
            limit: number;
 | 
			
		||||
        };
 | 
			
		||||
        /** Auth */
 | 
			
		||||
        Auth: {
 | 
			
		||||
@@ -247,6 +251,25 @@ export interface components {
 | 
			
		||||
            createdAt: string;
 | 
			
		||||
            status: components["schemas"]["AccountStatus"];
 | 
			
		||||
        };
 | 
			
		||||
        /** UserCreate */
 | 
			
		||||
        UserCreate: {
 | 
			
		||||
            /** Name */
 | 
			
		||||
            name?: string | null;
 | 
			
		||||
            /** Login */
 | 
			
		||||
            login?: string | null;
 | 
			
		||||
            /** Email */
 | 
			
		||||
            email?: string | null;
 | 
			
		||||
            /** Password */
 | 
			
		||||
            password?: string | null;
 | 
			
		||||
            /** Bindtenantid */
 | 
			
		||||
            bindTenantId?: string | null;
 | 
			
		||||
            role?: components["schemas"]["AccountRole"] | null;
 | 
			
		||||
            /** Meta */
 | 
			
		||||
            meta?: {
 | 
			
		||||
                [key: string]: unknown;
 | 
			
		||||
            } | null;
 | 
			
		||||
            status?: components["schemas"]["AccountStatus"] | null;
 | 
			
		||||
        };
 | 
			
		||||
        /** UserUpdate */
 | 
			
		||||
        UserUpdate: {
 | 
			
		||||
            /** Id */
 | 
			
		||||
@@ -257,6 +280,8 @@ export interface components {
 | 
			
		||||
            login?: string | null;
 | 
			
		||||
            /** Email */
 | 
			
		||||
            email?: string | null;
 | 
			
		||||
            /** Password */
 | 
			
		||||
            password?: string | null;
 | 
			
		||||
            /** Bindtenantid */
 | 
			
		||||
            bindTenantId?: string | null;
 | 
			
		||||
            role?: components["schemas"]["AccountRole"] | null;
 | 
			
		||||
@@ -435,7 +460,7 @@ export interface operations {
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": components["schemas"]["UserUpdate"];
 | 
			
		||||
                "application/json": components["schemas"]["UserCreate"];
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
@@ -445,7 +470,7 @@ export interface operations {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "application/json": components["schemas"]["User"];
 | 
			
		||||
                    "application/json": components["schemas"]["AllUser"];
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
            /** @description Validation Error */
 | 
			
		||||
@@ -476,7 +501,7 @@ export interface operations {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "application/json": components["schemas"]["User"];
 | 
			
		||||
                    "application/json": components["schemas"]["UserUpdate"];
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
            /** @description Validation Error */
 | 
			
		||||
@@ -511,7 +536,7 @@ export interface operations {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "application/json": components["schemas"]["User"];
 | 
			
		||||
                    "application/json": components["schemas"]["UserUpdate"];
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
            /** @description Validation Error */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,9 @@
 | 
			
		||||
import { components } from './openapi-types';
 | 
			
		||||
 | 
			
		||||
export type User = components['schemas']['User'];
 | 
			
		||||
export type AllUserResponse = components['schemas']['AllUserResponse'];
 | 
			
		||||
export type AllUser = components['schemas']['AllUser'];
 | 
			
		||||
export type AccountStatus = components['schemas']['AccountStatus'];
 | 
			
		||||
export type AccountRole = components['schemas']['AccountRole'];
 | 
			
		||||
export type UserUpdate = components['schemas']['UserUpdate'];
 | 
			
		||||
export type UserCreate = components['schemas']['UserCreate'];
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user