Compare commits
34 Commits
VORKOUT-15
...
master
Author | SHA1 | Date | |
---|---|---|---|
e6589a0950 | |||
a0fcc324fa | |||
|
2a35386c1d | ||
a936500101 | |||
4c0beb24f9 | |||
ad1369c3e3 | |||
5958f29ba8 | |||
784be40369 | |||
ad312d4ff8 | |||
ba65f36696 | |||
ad0a4837fc | |||
1eadd834e3 | |||
8f3fde623f | |||
edfd2c1159 | |||
3d8ee4835d | |||
9c9201f130 | |||
0eed0b0f20 | |||
7127d88524 | |||
692461e266 | |||
22064d2b52 | |||
febac9659f | |||
6c0a6ac1d4 | |||
a7e813b447 | |||
919758ab69 | |||
53bf173373 | |||
8f5dd07bf5 | |||
448e4264a5 | |||
e5dfdc3464 | |||
aae56a8c73 | |||
7c2c4071cc | |||
18bb79262c | |||
a3ee18f6fd | |||
71ab39a03c | |||
5d39065a7f |
18
Makefile
18
Makefile
@ -12,8 +12,7 @@ services:
|
|||||||
|
|
||||||
start-api:
|
start-api:
|
||||||
cd api && \
|
cd api && \
|
||||||
source .venv/bin/activate && \
|
poetry run python -m ${API_APPLICATION_NAME}
|
||||||
python -m ${API_APPLICATION_NAME}
|
|
||||||
|
|
||||||
start-client:
|
start-client:
|
||||||
cd client && \
|
cd client && \
|
||||||
@ -21,25 +20,21 @@ start-client:
|
|||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
cd api && \
|
cd api && \
|
||||||
source .venv/bin/activate && \
|
|
||||||
cd $(API_APPLICATION_NAME)/db && \
|
cd $(API_APPLICATION_NAME)/db && \
|
||||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args)
|
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic upgrade $(args)
|
||||||
|
|
||||||
downgrade:
|
downgrade:
|
||||||
cd api && \
|
cd api && \
|
||||||
source .venv/bin/activate && \
|
|
||||||
cd $(API_APPLICATION_NAME)/db && \
|
cd $(API_APPLICATION_NAME)/db && \
|
||||||
PYTHONPATH='../..' alembic downgrade -1
|
PYTHONPATH='../..' poetry run alembic downgrade -1
|
||||||
|
|
||||||
revision:
|
revision:
|
||||||
cd api && \
|
cd api && \
|
||||||
source .venv/bin/activate && \
|
|
||||||
cd $(API_APPLICATION_NAME)/db && \
|
cd $(API_APPLICATION_NAME)/db && \
|
||||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
|
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic revision --autogenerate
|
||||||
|
|
||||||
venv-api:
|
venv-api:
|
||||||
cd api && \
|
cd api && \
|
||||||
poetry env activate \
|
|
||||||
poetry install
|
poetry install
|
||||||
|
|
||||||
venv-client:
|
venv-client:
|
||||||
@ -62,3 +57,8 @@ format-api:
|
|||||||
check-api:
|
check-api:
|
||||||
cd api && \
|
cd api && \
|
||||||
poetry run ruff format . --check
|
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
|
import math
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from sqlalchemy import insert, select, func
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
|
||||||
from enum import Enum
|
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.db.tables.account import account_table
|
||||||
|
|
||||||
from api.schemas.account.account import User
|
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.
|
Получает список ползовелей заданных значениями 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)
|
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.
|
Получает юзера по 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_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
|
return None
|
||||||
|
|
||||||
user_data = {
|
return AllUser.model_validate(user)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[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()
|
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.
|
Создает нове поле в таблице account_table.
|
||||||
"""
|
"""
|
||||||
@ -117,14 +112,15 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int)
|
|||||||
email=user.email,
|
email=user.email,
|
||||||
bind_tenant_id=user.bind_tenant_id,
|
bind_tenant_id=user.bind_tenant_id,
|
||||||
role=user.role.value,
|
role=user.role.value,
|
||||||
meta=user.meta,
|
meta=user.meta or {},
|
||||||
creator_id=creator_id,
|
creator_id=creator_id,
|
||||||
created_at=datetime.now(timezone.utc),
|
created_at=datetime.now(timezone.utc),
|
||||||
status=user.status.value,
|
status=user.status.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
await connection.execute(query)
|
res = await connection.execute(query)
|
||||||
|
|
||||||
await connection.commit()
|
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 import User
|
||||||
from api.schemas.account.account_keyring import AccountKeyring
|
from api.schemas.account.account_keyring import AccountKeyring
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
|
|
||||||
from api.utils.key_id_gen import KeyIdGenerator
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
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 = (
|
query = (
|
||||||
select(account_table, account_keyring_table)
|
select(account_table, account_keyring_table)
|
||||||
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
|
.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
|
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)
|
password = AccountKeyring.model_validate(password_data)
|
||||||
return user, password
|
return user, password
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
from typing import Optional
|
from datetime import datetime, timedelta, timezone
|
||||||
from datetime import datetime, timezone
|
|
||||||
from enum import Enum
|
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 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.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]:
|
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()
|
await connection.commit()
|
||||||
|
|
||||||
return key
|
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("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False),
|
||||||
Column("key_type", SQLAEnum(KeyType), 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_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("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||||
Column("expiry", DateTime(timezone=True), nullable=True),
|
Column("expiry", DateTime(timezone=True), nullable=True),
|
||||||
Column("status", SQLAEnum(KeyStatus), nullable=False),
|
Column("status", SQLAEnum(KeyStatus), nullable=False),
|
||||||
|
@ -4,29 +4,24 @@ from fastapi import (
|
|||||||
HTTPException,
|
HTTPException,
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
from api.db.connection.session import get_connection_dep
|
from api.db.connection.session import get_connection_dep
|
||||||
|
|
||||||
from api.db.logic.account import (
|
from api.db.logic.account import (
|
||||||
get_user_by_id,
|
|
||||||
update_user_by_id,
|
|
||||||
create_user,
|
create_user,
|
||||||
get_user_by_login,
|
|
||||||
get_user_accaunt_page,
|
get_user_accaunt_page,
|
||||||
|
get_user_by_id,
|
||||||
|
get_user_by_login,
|
||||||
|
update_user_by_id,
|
||||||
)
|
)
|
||||||
|
from api.db.logic.keyring import create_password_key, update_password_key
|
||||||
from api.schemas.account.account import User
|
|
||||||
from api.db.tables.account import AccountStatus
|
from api.db.tables.account import AccountStatus
|
||||||
|
from api.schemas.account.account import User
|
||||||
from api.schemas.base import bearer_schema
|
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.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.update_data_validation import update_user_data_changes
|
||||||
|
from api.services.user_role_validation import db_user_role_validation
|
||||||
|
|
||||||
api_router = APIRouter(
|
api_router = APIRouter(
|
||||||
prefix="/account",
|
prefix="/account",
|
||||||
@ -51,9 +46,11 @@ async def get_all_account(
|
|||||||
return user_list
|
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(
|
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)
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
@ -65,26 +62,27 @@ async def get_account(
|
|||||||
return user
|
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(
|
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)
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
user_validation = await get_user_by_login(connection, user.login)
|
user_validation = await get_user_by_login(connection, user.login)
|
||||||
|
|
||||||
if user_validation is None:
|
if user_validation is None:
|
||||||
await create_user(connection, user, authorize_user.id)
|
new_user = await create_user(connection, user, authorize_user.id)
|
||||||
user_new = await get_user_by_login(connection, user.login)
|
await create_password_key(connection, user.password, new_user.id)
|
||||||
return user_new
|
return new_user
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
|
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(
|
async def update_account(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
user_update: UserUpdate,
|
user_update: UserUpdate,
|
||||||
@ -97,12 +95,15 @@ async def update_account(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
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)
|
update_values = update_user_data_changes(user_update, user)
|
||||||
|
|
||||||
if update_values is None:
|
if update_values is None:
|
||||||
return user
|
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)
|
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)
|
@api_router.delete("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
async def delete_account(
|
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)
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import EmailStr, Field, TypeAdapter
|
from pydantic import EmailStr, Field, TypeAdapter
|
||||||
|
|
||||||
from api.db.tables.account import AccountRole, AccountStatus
|
from api.db.tables.account import AccountRole, AccountStatus
|
||||||
|
|
||||||
from api.schemas.base import Base
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ class UserUpdate(Base):
|
|||||||
name: Optional[str] = Field(None, max_length=100)
|
name: Optional[str] = Field(None, max_length=100)
|
||||||
login: Optional[str] = Field(None, max_length=100)
|
login: Optional[str] = Field(None, max_length=100)
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
|
password: Optional[str] = None
|
||||||
bind_tenant_id: Optional[str] = Field(None, max_length=40)
|
bind_tenant_id: Optional[str] = Field(None, max_length=40)
|
||||||
role: Optional[AccountRole] = None
|
role: Optional[AccountRole] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
@ -20,6 +21,17 @@ class UserUpdate(Base):
|
|||||||
status: Optional[AccountStatus] = None
|
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):
|
class AllUser(Base):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
@ -35,6 +47,8 @@ class AllUserResponse(Base):
|
|||||||
users: List[AllUser]
|
users: List[AllUser]
|
||||||
amount_count: int
|
amount_count: int
|
||||||
amount_pages: int
|
amount_pages: int
|
||||||
|
current_page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
all_user_adapter = TypeAdapter(List[AllUser])
|
all_user_adapter = TypeAdapter(List[AllUser])
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
from fastapi import Request, HTTPException
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
from api.db.logic.auth import get_user
|
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.db.tables.account import AccountStatus
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
from api.utils.hasher import Hasher
|
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"):
|
if not hasattr(request.state, "current_user"):
|
||||||
return HTTPException(status_code=401, detail="Unauthorized")
|
return HTTPException(status_code=401, detail="Unauthorized")
|
||||||
return request.state.current_user
|
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)
|
sql_user, sql_password = await get_user(connection, username)
|
||||||
|
|
||||||
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
||||||
return None
|
return None
|
||||||
hasher = Hasher()
|
|
||||||
if not hasher.verify_data(password, sql_password.key_value):
|
if not hasher.verify_data(password, sql_password.key_value):
|
||||||
return None
|
return None
|
||||||
return sql_user
|
return sql_user
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
# Хешер для работы с паролем.
|
# Хешер для работы с паролем.
|
||||||
|
|
||||||
@ -14,3 +16,10 @@ class Hasher:
|
|||||||
def verify_data(self, password: str, hashed: str) -> bool:
|
def verify_data(self, password: str, hashed: str) -> bool:
|
||||||
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
||||||
return self.hash_data(password) == hashed
|
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 asyncio
|
||||||
import hashlib
|
import os
|
||||||
import secrets
|
|
||||||
|
|
||||||
from api.db.connection.session import get_connection
|
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
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
INIT_LOCK_FILE = "../init.lock"
|
INIT_LOCK_FILE = "../init.lock"
|
||||||
DEFAULT_LOGIN = "vorkout"
|
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():
|
async def init():
|
||||||
if os.path.exists(INIT_LOCK_FILE):
|
if os.path.exists(INIT_LOCK_FILE):
|
||||||
print("Sorry, service is already initialized")
|
print("Sorry, service is already initialized")
|
||||||
return
|
return
|
||||||
|
|
||||||
async with get_connection() as conn:
|
async with get_connection() as conn:
|
||||||
password = generate_password()
|
password = hasher.generate_password()
|
||||||
hashed_password = hash_password(password)
|
hashed_password = hasher.hash_data(password)
|
||||||
|
|
||||||
create_user_query = account_table.insert().values(
|
create_user_query = account_table.insert().values(
|
||||||
name=DEFAULT_LOGIN,
|
name=DEFAULT_LOGIN,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.0.4"
|
version = "0.0.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
|
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.3",
|
"version": "0.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^5.6.1",
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
// import { Auth, Tokens } from '../types/auth';
|
|
||||||
import axiosRetry from 'axios-retry';
|
import axiosRetry from 'axios-retry';
|
||||||
import { Auth, Tokens } from '@/types/auth';
|
import { Auth, Tokens } from '@/types/auth';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { AuthService } from '@/services/authService';
|
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}://${
|
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
|
||||||
import.meta.env.VITE_APP_API_URL
|
import.meta.env.VITE_APP_API_URL
|
||||||
@ -109,6 +108,23 @@ const api = {
|
|||||||
);
|
);
|
||||||
return response.data;
|
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;
|
export default api;
|
||||||
|
@ -2,12 +2,16 @@ import { Drawer } from 'antd';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Avatar, Typography } from 'antd';
|
import { Avatar, Typography } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
|
||||||
interface ContentDrawerProps {
|
interface ContentDrawerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
closeDrawer: () => void;
|
closeDrawer: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
type: 'create' | 'edit';
|
type: 'create' | 'edit';
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContentDrawer({
|
export default function ContentDrawer({
|
||||||
@ -15,7 +19,11 @@ export default function ContentDrawer({
|
|||||||
closeDrawer,
|
closeDrawer,
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
|
login,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
}: ContentDrawerProps) {
|
}: ContentDrawerProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [width, setWidth] = useState<number | string>('30%');
|
const [width, setWidth] = useState<number | string>('30%');
|
||||||
|
|
||||||
@ -30,6 +38,7 @@ export default function ContentDrawer({
|
|||||||
window.addEventListener('resize', calculateWidths);
|
window.addEventListener('resize', calculateWidths);
|
||||||
return () => window.removeEventListener('resize', calculateWidths);
|
return () => window.removeEventListener('resize', calculateWidths);
|
||||||
}, []);
|
}, []);
|
||||||
|
console.log(login, user?.login, login === user?.login);
|
||||||
|
|
||||||
const editDrawerTitle = (
|
const editDrawerTitle = (
|
||||||
<div
|
<div
|
||||||
@ -59,16 +68,21 @@ export default function ContentDrawer({
|
|||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
||||||
<Avatar
|
<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}
|
size={40}
|
||||||
style={{ flexShrink: 0 }}
|
style={{ flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<div>
|
<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>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||||
alexandralex@vorkout.ru
|
{email}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -152,7 +166,7 @@ export default function ContentDrawer({
|
|||||||
placement="right"
|
placement="right"
|
||||||
open={open}
|
open={open}
|
||||||
width={width}
|
width={width}
|
||||||
destroyOnClose={true}
|
destroyOnHidden={true}
|
||||||
closable={false}
|
closable={false}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
import { Avatar } from 'antd';
|
import { Avatar } from 'antd';
|
||||||
import Title from 'antd/es/typography/Title';
|
import Title from 'antd/es/typography/Title';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import ContentDrawer from './ContentDrawer';
|
||||||
|
import UserEdit from './UserEdit';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -7,6 +11,13 @@ interface HeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ title, additionalContent }: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -43,13 +54,24 @@ export default function Header({ title, additionalContent }: HeaderProps) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
|
onClick={showEditDrawer}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={25.77}
|
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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
import { Divider, Menu, Tooltip } from 'antd';
|
import { Divider, Menu, Tooltip } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -13,6 +14,7 @@ export default function SiderMenu({
|
|||||||
selectedKey,
|
selectedKey,
|
||||||
hangleMenuClick,
|
hangleMenuClick,
|
||||||
}: SiderMenuProps) {
|
}: SiderMenuProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const collapseStyle = collapsed
|
const collapseStyle = collapsed
|
||||||
? { fontSize: '12px' }
|
? { fontSize: '12px' }
|
||||||
@ -74,15 +76,17 @@ export default function SiderMenu({
|
|||||||
label: t('settings'),
|
label: t('settings'),
|
||||||
className: 'no-expand-icon',
|
className: 'no-expand-icon',
|
||||||
children: [
|
children: [
|
||||||
{
|
user && (user.role === 'OWNER' || user.role === 'ADMIN')
|
||||||
key: '/accounts',
|
? {
|
||||||
label: !collapsed ? (
|
key: '/accounts',
|
||||||
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
|
label: !collapsed ? (
|
||||||
) : (
|
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
|
||||||
t('accounts')
|
) : (
|
||||||
),
|
t('accounts')
|
||||||
style: collapseStyle,
|
),
|
||||||
},
|
style: collapseStyle,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
{
|
{
|
||||||
key: '/events-list',
|
key: '/events-list',
|
||||||
label: !collapsed ? (
|
label: !collapsed ? (
|
||||||
|
@ -8,13 +8,19 @@ import {
|
|||||||
UploadFile,
|
UploadFile,
|
||||||
GetProp,
|
GetProp,
|
||||||
UploadProps,
|
UploadProps,
|
||||||
} from 'antd';
|
message,
|
||||||
import { useState } from 'react';
|
Spin,
|
||||||
import { useTranslation } from 'react-i18next';
|
} 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;
|
const { Option } = Select;
|
||||||
|
|
||||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
|
||||||
|
|
||||||
const getBase64 = (file: FileType): Promise<string> =>
|
const getBase64 = (file: FileType): Promise<string> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@ -24,10 +30,17 @@ const getBase64 = (file: FileType): Promise<string> =>
|
|||||||
reader.onerror = (error) => reject(error);
|
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 { t } = useTranslation();
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
const [previewImage, setPreviewImage] = useState('');
|
const [previewImage, setPreviewImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
@ -40,40 +53,49 @@ export default function UserCreate() {
|
|||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
|
||||||
setFileList(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 = (
|
const customUploadButton = (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: '102px',
|
height: "102px",
|
||||||
width: '102px',
|
width: "102px",
|
||||||
backgroundColor: '#E2E2E2',
|
backgroundColor: "#E2E2E2",
|
||||||
borderRadius: '50%',
|
borderRadius: "50%",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginTop: 30,
|
marginTop: 30,
|
||||||
cursor: 'pointer',
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="./icons/drawer/add_photo_alternate.svg"
|
src="./icons/drawer/add_photo_alternate.svg"
|
||||||
alt="add_photo_alternate"
|
alt="add_photo_alternate"
|
||||||
style={{ height: '18px', width: '18px' }}
|
style={{ height: "18px", width: "18px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
<span style={{ fontSize: "14px", color: "#8c8c8c" }}>
|
||||||
{t('selectPhoto')}
|
{t("selectPhoto")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const photoToUpload = (
|
const photoToUpload = (
|
||||||
<div style={{ height: '102px' }}>
|
<div style={{ height: "102px" }}>
|
||||||
<Upload
|
<Upload
|
||||||
listType="picture-circle"
|
listType="picture-circle"
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
@ -85,11 +107,11 @@ export default function UserCreate() {
|
|||||||
</Upload>
|
</Upload>
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Image
|
<Image
|
||||||
wrapperStyle={{ display: 'none' }}
|
wrapperStyle={{ display: "none" }}
|
||||||
preview={{
|
preview={{
|
||||||
visible: previewOpen,
|
visible: previewOpen,
|
||||||
onVisibleChange: (visible) => setPreviewOpen(visible),
|
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||||
afterOpenChange: (visible) => !visible && setPreviewImage(''),
|
afterOpenChange: (visible) => !visible && setPreviewImage(""),
|
||||||
}}
|
}}
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
/>
|
/>
|
||||||
@ -100,24 +122,24 @@ export default function UserCreate() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
marginBottom: '36px',
|
marginBottom: "36px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{photoToUpload}
|
{photoToUpload}
|
||||||
@ -126,83 +148,85 @@ export default function UserCreate() {
|
|||||||
<Form
|
<Form
|
||||||
name="user-edit-form"
|
name="user-edit-form"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
// onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
name: '',
|
name: "",
|
||||||
login: '',
|
login: "",
|
||||||
password: '',
|
password: "",
|
||||||
email: '',
|
email: "",
|
||||||
tenant: '',
|
bindTenantId: "",
|
||||||
role: '',
|
role: "",
|
||||||
status: '',
|
status: "",
|
||||||
}}
|
}}
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('name')}
|
label={t("name")}
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true, message: t('nameMessage') }]}
|
rules={[{ required: true, message: t("nameMessage") }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('login')}
|
label={t("login")}
|
||||||
name="login"
|
name="login"
|
||||||
rules={[{ required: true, message: t('loginMessage') }]}
|
rules={[{ required: true, message: t("loginMessage") }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('password')}
|
label={t("password")}
|
||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
rules={[{ message: t("passwordMessage") }]}
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('email')}
|
label={t("email")}
|
||||||
name="email"
|
name="email"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: t('emailMessage') },
|
{ message: t("emailMessage") },
|
||||||
{ type: 'email', message: t('emailErrorMessage') },
|
{ type: "email", message: t("emailErrorMessage") },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('tenant')}
|
label={t("tenant")}
|
||||||
name="tenant"
|
name="bindTenantId"
|
||||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
rules={[{ message: t("tenantMessage") }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('role')}
|
label={t("role")}
|
||||||
name="role"
|
name="role"
|
||||||
rules={[{ required: true, message: t('roleMessage') }]}
|
rules={[{ required: true, message: t("roleMessage") }]}
|
||||||
>
|
>
|
||||||
<Select placeholder={t('roleMessage')}>
|
<Select placeholder={t("roleMessage")}>
|
||||||
<Option value="Директор магазина">Директор магазина</Option>
|
{user && user.role === "OWNER" ? (
|
||||||
<Option value="Менеджер">Менеджер</Option>
|
<Option value="ADMIN">{t("ADMIN")}</Option>
|
||||||
<Option value="Кассир">Кассир</Option>
|
) : undefined}
|
||||||
|
<Option value="EDITOR">{t("EDITOR")}</Option>
|
||||||
|
<Option value="VIEWER">{t("VIEWER")}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('status')}
|
label={t("status")}
|
||||||
name="status"
|
name="status"
|
||||||
rules={[{ required: true, message: t('statusMessage') }]}
|
rules={[{ required: true, message: t("statusMessage") }]}
|
||||||
>
|
>
|
||||||
<Select placeholder={t('statusMessage')}>
|
<Select placeholder={t("statusMessage")}>
|
||||||
<Option value="ACTIVE">Активен</Option>
|
<Option value="ACTIVE">{t("ACTIVE")}</Option>
|
||||||
<Option value="DISABLED">Неактивен</Option>
|
<Option value="DISABLED">{t("DISABLED")}</Option>
|
||||||
<Option value="BLOCKED">Заблокирован</Option>
|
<Option value="BLOCKED">{t("BLOCKED")}</Option>
|
||||||
<Option value="DELETED">Удален</Option>
|
<Option value="DELETED">{t("DELETED")}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@ -213,14 +237,23 @@ export default function UserCreate() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
block
|
block
|
||||||
style={{ color: '#000' }}
|
style={{ color: "#000" }}
|
||||||
>
|
>
|
||||||
<img
|
{loading ? (
|
||||||
src="/icons/drawer/reg.svg"
|
<>
|
||||||
alt="save"
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "}
|
||||||
style={{ height: '18px', width: '18px' }}
|
{t("saving")}
|
||||||
/>{' '}
|
</>
|
||||||
{t('addAccount')}
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/reg.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: "18px", width: "18px" }}
|
||||||
|
/>{" "}
|
||||||
|
{t("addAccount")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</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';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const { Option } = Select;
|
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 { 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 (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
<Form
|
<Form
|
||||||
|
form={form}
|
||||||
name="user-edit-form"
|
name="user-edit-form"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
// onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
initialValues={{
|
initialValues={{ ...user }}
|
||||||
name: 'Александр Александров',
|
|
||||||
login: 'alexandralex@vorkout.ru',
|
|
||||||
password: 'jKUUl776GHd',
|
|
||||||
email: 'alexandralex@vorkout.ru',
|
|
||||||
tenant: 'text',
|
|
||||||
role: 'Директор магазина',
|
|
||||||
status: 'Активен',
|
|
||||||
}}
|
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('name')}
|
label={t('name')}
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true, message: t('nameMessage') }]}
|
rules={[{ message: t('nameMessage') }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
label={t('login')}
|
<Form.Item
|
||||||
name="login"
|
label={t('login')}
|
||||||
rules={[{ required: true, message: t('loginMessage') }]}
|
name="login"
|
||||||
>
|
rules={[{ message: t('loginMessage') }]}
|
||||||
<Input />
|
>
|
||||||
</Form.Item>
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('password')}
|
label={t('password')}
|
||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
rules={[{ message: t('passwordMessage') }]}
|
||||||
>
|
>
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -59,23 +113,27 @@ export default function UserEdit() {
|
|||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('tenant')}
|
label={t('tenant')}
|
||||||
name="tenant"
|
name="bindTenantId"
|
||||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
label={t('role')}
|
<Form.Item
|
||||||
name="role"
|
label={t('role')}
|
||||||
rules={[{ required: true, message: t('roleMessage') }]}
|
name="role"
|
||||||
>
|
rules={[{ required: true, message: t('roleMessage') }]}
|
||||||
<Select placeholder={t('roleMessage')}>
|
>
|
||||||
<Option value="Директор магазина">Директор магазина</Option>
|
<Select placeholder={t('roleMessage')}>
|
||||||
<Option value="Менеджер">Менеджер</Option>
|
{currentUser && currentUser.role === 'OWNER' ? (
|
||||||
<Option value="Кассир">Кассир</Option>
|
<Option value="ADMIN">{t('ADMIN')}</Option>
|
||||||
</Select>
|
) : undefined}
|
||||||
</Form.Item>
|
<Option value="EDITOR">{t('EDITOR')}</Option>
|
||||||
|
<Option value="VIEWER">{t('VIEWER')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('status')}
|
label={t('status')}
|
||||||
@ -83,10 +141,10 @@ export default function UserEdit() {
|
|||||||
rules={[{ required: true, message: t('statusMessage') }]}
|
rules={[{ required: true, message: t('statusMessage') }]}
|
||||||
>
|
>
|
||||||
<Select placeholder={t('statusMessage')}>
|
<Select placeholder={t('statusMessage')}>
|
||||||
<Option value="ACTIVE">Активен</Option>
|
<Option value="ACTIVE">{t('ACTIVE')}</Option>
|
||||||
<Option value="DISABLED">Неактивен</Option>
|
<Option value="DISABLED">{t('DISABLED')}</Option>
|
||||||
<Option value="BLOCKED">Заблокирован</Option>
|
<Option value="BLOCKED">{t('BLOCKED')}</Option>
|
||||||
<Option value="DELETED">Удален</Option>
|
<Option value="DELETED">{t('DELETED')}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@ -99,12 +157,21 @@ export default function UserEdit() {
|
|||||||
block
|
block
|
||||||
style={{ color: '#000' }}
|
style={{ color: '#000' }}
|
||||||
>
|
>
|
||||||
<img
|
{loading ? (
|
||||||
src="/icons/drawer/save.svg"
|
<>
|
||||||
alt="save"
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '}
|
||||||
style={{ height: '18px', width: '18px' }}
|
{t('saving')}
|
||||||
/>{' '}
|
</>
|
||||||
{t('save')}
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/save.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: '18px', width: '18px' }}
|
||||||
|
/>{' '}
|
||||||
|
{t('save')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -37,6 +37,20 @@ i18n
|
|||||||
addAccount: 'Add account',
|
addAccount: 'Add account',
|
||||||
save: 'Save changes',
|
save: 'Save changes',
|
||||||
newAccount: 'New account',
|
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: {
|
ru: {
|
||||||
@ -66,6 +80,20 @@ i18n
|
|||||||
addAccount: 'Добавить аккаунт',
|
addAccount: 'Добавить аккаунт',
|
||||||
save: 'Сохранить изменения',
|
save: 'Сохранить изменения',
|
||||||
newAccount: 'Новая учетная запись',
|
newAccount: 'Новая учетная запись',
|
||||||
|
ACTIVE: 'Активен',
|
||||||
|
DISABLED: 'Выключен',
|
||||||
|
BLOCKED: 'Заблокирован',
|
||||||
|
DELETED: 'Удален',
|
||||||
|
OWNER: 'Владелец',
|
||||||
|
ADMIN: 'Админ',
|
||||||
|
EDITOR: 'Редактор',
|
||||||
|
VIEWER: 'Наблюдатель',
|
||||||
|
nameLogin: 'Имя, Логин',
|
||||||
|
createdAt: 'Создано',
|
||||||
|
saving: 'Сохранение...',
|
||||||
|
createdAccountMessage: 'Пользователь успешно создан!',
|
||||||
|
editAccountMessage: 'Пользователь успешно обновлен!',
|
||||||
|
you: '(Вы)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,39 +1,219 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from "react-i18next";
|
||||||
import { User } from '@/types/user';
|
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
|
||||||
import Header from '@/components/Header';
|
import Header from "@/components/Header";
|
||||||
import ContentDrawer from '@/components/ContentDrawer';
|
import ContentDrawer from "@/components/ContentDrawer";
|
||||||
import UserCreate from '@/components/UserCreate';
|
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() {
|
export default function AccountsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [openCreate, setOpenCreate] = useState(false);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const showDrawer = () => setOpen(true);
|
const [activeAccount, setActiveAccount] = useState<
|
||||||
const closeDrawer = () => setOpen(false);
|
{ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
title={t('accounts')}
|
title={t("accounts")}
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<img
|
<img
|
||||||
src="./icons/header/add_2.svg"
|
src="./icons/header/add_2.svg"
|
||||||
alt="add"
|
alt="add"
|
||||||
style={{
|
style={{
|
||||||
height: '18px',
|
height: "18px",
|
||||||
width: '18px',
|
width: "18px",
|
||||||
cursor: 'pointer',
|
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">
|
<ContentDrawer
|
||||||
<UserCreate />
|
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>
|
</ContentDrawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Form, Input, Button, Typography } from 'antd';
|
import { Form, Input, Button, Typography, message } from 'antd';
|
||||||
import {
|
import {
|
||||||
EyeInvisibleOutlined,
|
EyeInvisibleOutlined,
|
||||||
EyeTwoTone,
|
EyeTwoTone,
|
||||||
@ -45,7 +45,11 @@ export default function LoginPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form name="login" onFinish={onFinish} layout="vertical">
|
<Form
|
||||||
|
name="login"
|
||||||
|
onFinish={onFinish}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="login"
|
name="login"
|
||||||
rules={[{ required: true, message: 'Введите login' }]}
|
rules={[{ required: true, message: 'Введите login' }]}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import api from '@/api/api';
|
import api from '@/api/api';
|
||||||
import { User } from '@/types/user';
|
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
static async getProfile(): Promise<User> {
|
static async getProfile(): Promise<User> {
|
||||||
@ -9,8 +9,30 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
|
static async getUsers(
|
||||||
const users = api.getUsers(page, limit);
|
page: number = 1,
|
||||||
return users;
|
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;
|
amountCount: number;
|
||||||
/** Amountpages */
|
/** Amountpages */
|
||||||
amountPages: number;
|
amountPages: number;
|
||||||
|
/** Currentpage */
|
||||||
|
currentPage: number;
|
||||||
|
/** Limit */
|
||||||
|
limit: number;
|
||||||
};
|
};
|
||||||
/** Auth */
|
/** Auth */
|
||||||
Auth: {
|
Auth: {
|
||||||
@ -247,6 +251,25 @@ export interface components {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
status: components["schemas"]["AccountStatus"];
|
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 */
|
||||||
UserUpdate: {
|
UserUpdate: {
|
||||||
/** Id */
|
/** Id */
|
||||||
@ -257,6 +280,8 @@ export interface components {
|
|||||||
login?: string | null;
|
login?: string | null;
|
||||||
/** Email */
|
/** Email */
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
/** Password */
|
||||||
|
password?: string | null;
|
||||||
/** Bindtenantid */
|
/** Bindtenantid */
|
||||||
bindTenantId?: string | null;
|
bindTenantId?: string | null;
|
||||||
role?: components["schemas"]["AccountRole"] | null;
|
role?: components["schemas"]["AccountRole"] | null;
|
||||||
@ -435,7 +460,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["UserUpdate"];
|
"application/json": components["schemas"]["UserCreate"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@ -445,7 +470,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["User"];
|
"application/json": components["schemas"]["AllUser"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@ -476,7 +501,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["User"];
|
"application/json": components["schemas"]["UserUpdate"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@ -511,7 +536,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["User"];
|
"application/json": components["schemas"]["UserUpdate"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
import { components } from './openapi-types';
|
import { components } from './openapi-types';
|
||||||
|
|
||||||
export type User = components['schemas']['User'];
|
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'];
|
||||||
|
Loading…
Reference in New Issue
Block a user