Compare commits

..

6 Commits

Author SHA1 Message Date
2493cd0d9f fix(api): fix shadows an attribute in parent in ListEvents for field schema 2025-07-02 14:14:03 +05:00
TheNoxium
f65f4caf5c fix: description 2025-07-02 11:48:23 +05:00
TheNoxium
a90a802659 fix: update data 2025-07-02 11:43:24 +05:00
TheNoxium
3d5d717962 fix: removed id for update data 2025-07-02 11:37:10 +05:00
TheNoxium
0bff556b10 fix: page schems, name 2025-07-01 22:31:16 +05:00
TheNoxium
f550b86c68 feat: CRUD ListEvent 2025-06-30 15:37:07 +05:00
33 changed files with 700 additions and 783 deletions

View File

@ -12,7 +12,8 @@ services:
start-api: start-api:
cd api && \ cd api && \
poetry run python -m ${API_APPLICATION_NAME} source .venv/bin/activate && \
python -m ${API_APPLICATION_NAME}
start-client: start-client:
cd client && \ cd client && \
@ -20,21 +21,25 @@ 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 poetry run alembic upgrade $(args) PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args)
downgrade: downgrade:
cd api && \ cd api && \
source .venv/bin/activate && \
cd $(API_APPLICATION_NAME)/db && \ cd $(API_APPLICATION_NAME)/db && \
PYTHONPATH='../..' poetry run alembic downgrade -1 PYTHONPATH='../..' 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 poetry run alembic revision --autogenerate PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
venv-api: venv-api:
cd api && \ cd api && \
poetry env activate \
poetry install poetry install
venv-client: venv-client:
@ -57,8 +62,3 @@ 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

View File

@ -1,38 +0,0 @@
"""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 ###

View File

@ -1,17 +1,19 @@
import math
from datetime import datetime, timezone
from enum import Enum
from typing import Optional from typing import Optional
import math
from sqlalchemy import func, insert, select from datetime import datetime, timezone
from sqlalchemy import insert, select, func
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from enum import Enum
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 all_user_adapter, AllUser, AllUserResponse, UserCreate from api.schemas.endpoints.account import AllUserResponse, all_user_adapter
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]: async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[User]:
""" """
Получает список ползовелей заданных значениями page, limit. Получает список ползовелей заданных значениями page, limit.
""" """
@ -45,28 +47,31 @@ 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( return AllUserResponse(users=validated_users, amount_count=total_count, amount_pages=total_pages)
users=validated_users,
amount_count=total_count,
amount_pages=total_pages,
current_page=page,
limit=limit,
)
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]: async def get_user_by_id(connection: AsyncConnection, id: int) -> Optional[User]:
""" """
Получает юзера по id. Получает юзера по id.
""" """
query = select(account_table).where(account_table.c.id == user_id) query = select(account_table).where(account_table.c.id == id)
user_db_cursor = await connection.execute(query) user_db_cursor = await connection.execute(query)
user = user_db_cursor.mappings().one_or_none() user_db = user_db_cursor.one_or_none()
if not user: if not user_db:
return None return None
return AllUser.model_validate(user) 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)
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]: async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
@ -102,7 +107,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: UserCreate, creator_id: int) -> Optional[AllUser]: async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]:
""" """
Создает нове поле в таблице account_table. Создает нове поле в таблице account_table.
""" """
@ -112,15 +117,14 @@ async def create_user(connection: AsyncConnection, user: UserCreate, creator_id:
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 or {}, meta=user.meta,
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,
) )
res = await connection.execute(query) await connection.execute(query)
await connection.commit() await connection.commit()
new_user = await get_user_by_id(connection, res.lastrowid)
return new_user return user

View File

@ -8,14 +8,13 @@ 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) -> tuple[Optional[AllUser], Optional[AccountKeyring]]: async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
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)
@ -46,7 +45,7 @@ async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[Al
for column in account_keyring_table.columns for column in account_keyring_table.columns
} }
user = AllUser.model_validate(user_data) user = User.model_validate(user_data)
password = AccountKeyring.model_validate(password_data) password = AccountKeyring.model_validate(password_data)
return user, password return user, password

View File

@ -1,14 +1,13 @@
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Optional from typing import Optional
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import insert, select, update from sqlalchemy import insert, select
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, KeyStatus, KeyType from api.db.tables.account import account_keyring_table
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]:
@ -68,37 +67,3 @@ 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()

View File

@ -0,0 +1,174 @@
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 api.db.tables.events import list_events_table
from api.schemas.events.list_events import ListEvent
from api.schemas.endpoints.list_events import all_list_event_adapter,AllListEventResponse
async def get_listevents_page_by_creator_id(connection: AsyncConnection, creator_id: int, page: int, limit: int) -> Optional[AllListEventResponse]:
"""
Получает список событий заданного создателя по значениям page и limit и creator_id.
"""
first_event = page * limit - limit
query = (
select(
list_events_table.c.id,
list_events_table.c.name,
list_events_table.c.title,
list_events_table.c.creator_id,
list_events_table.c.created_at,
list_events_table.c.schema_,
list_events_table.c.state,
list_events_table.c.status,
)
.where(list_events_table.c.creator_id == creator_id) # Фильтрация по creator_id
.order_by(list_events_table.c.id)
.offset(first_event)
.limit(limit)
)
count_query = (
select(func.count())
.select_from(list_events_table)
.where(list_events_table.c.creator_id == creator_id) # Фильтрация по creator_id
)
result = await connection.execute(query)
count_result = await connection.execute(count_query)
events_data = result.mappings().all()
total_count = count_result.scalar()
total_pages = math.ceil(total_count / limit)
# Здесь предполагается, что all_list_event_adapter.validate_python корректно обрабатывает данные
validated_list_event = all_list_event_adapter.validate_python(events_data)
return AllListEventResponse(list_event=validated_list_event, amount_count=total_count, amount_pages=total_pages, current_page=page,
limit = limit)
async def get_listevents_page(connection: AsyncConnection, page, limit) -> Optional[AllListEventResponse]:
"""
Получает список событий заданного создателя по значениям page и limit.
"""
first_event = page * limit - (limit)
query = (
select(
list_events_table.c.id,
list_events_table.c.name,
list_events_table.c.title,
list_events_table.c.creator_id,
list_events_table.c.created_at,
list_events_table.c.schema,
list_events_table.c.state,
list_events_table.c.status,
)
.order_by(list_events_table.c.id)
.offset(first_event)
.limit(limit)
)
count_query = select(func.count()).select_from(list_events_table)
result = await connection.execute(query)
count_result = await connection.execute(count_query)
events_data = result.mappings().all()
total_count = count_result.scalar()
total_pages = math.ceil(total_count / limit)
# Здесь предполагается, что all_list_event_adapter.validate_python корректно обрабатывает данные
validated_list_event = all_list_event_adapter.validate_python(events_data)
return AllListEventResponse(list_event=validated_list_event, amount_count=total_count, amount_pages=total_pages,current_page=page,
limit = limit)
async def get_listevents_by_name(connection: AsyncConnection, name: str) -> Optional[ListEvent]:
"""
Получает list events по name.
"""
query = select(list_events_table).where(list_events_table.c.name == name)
listevents_db_cursor = await connection.execute(query)
listevents_db = listevents_db_cursor.one_or_none()
if not listevents_db:
return None
listevents_data = {
column.name: (
getattr(listevents_db, column.name).name
if isinstance(getattr(listevents_db, column.name), Enum)
else getattr(listevents_db, column.name)
)
for column in list_events_table.columns
}
return ListEvent.model_validate(listevents_data)
async def get_listevents_by_id(connection: AsyncConnection, id: int) -> Optional[ListEvent]:
"""
Получает listevent по id.
"""
query = select(list_events_table).where(list_events_table.c.id == id)
listevents_db_cursor = await connection.execute(query)
listevents_db = listevents_db_cursor.one_or_none()
if not listevents_db:
return None
listevents_data = {
column.name: (
getattr(listevents_db, column.name).name
if isinstance(getattr(listevents_db, column.name), Enum)
else getattr(listevents_db, column.name)
)
for column in list_events_table.columns
}
return ListEvent.model_validate(listevents_data)
async def update_listevents_by_id(connection: AsyncConnection, update_values, listevents):
"""
Вносит изменеия в нужное поле таблицы list_events_table.
"""
await connection.execute(list_events_table.update().where(list_events_table.c.id == listevents.id).values(**update_values))
await connection.commit()
async def create_listevents(connection: AsyncConnection, listevents: ListEvent, creator_id: int) -> Optional[ListEvent]:
"""
Создает нове поле в таблице list_events_table.
"""
query = insert(list_events_table).values(
name=listevents.name,
title=listevents.title, # добавлено поле title
creator_id=creator_id,
created_at=datetime.now(timezone.utc),
schema=listevents.schema_, # добавлено поле schema
state=listevents.state.value, # добавлено поле state
status=listevents.status.value # добавлено поле status
)
await connection.execute(query)
await connection.commit()
return listevents

View File

@ -3,8 +3,6 @@ import enum
from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func from sqlalchemy.sql import func
from enum import Enum
from api.db.sql_types import UnsignedInt from api.db.sql_types import UnsignedInt
from api.db import metadata from api.db import metadata
@ -60,7 +58,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(512), nullable=False), Column("key_value", String(255), 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),

View File

@ -2,8 +2,9 @@ from api.endpoints.auth import api_router as auth_router
from api.endpoints.profile import api_router as profile_router from api.endpoints.profile import api_router as profile_router
from api.endpoints.account import api_router as account_router from api.endpoints.account import api_router as account_router
from api.endpoints.keyring import api_router as keyring_router from api.endpoints.keyring import api_router as keyring_router
from api.endpoints.listevents import api_router as listevents_router
list_of_routes = [auth_router, profile_router, account_router, keyring_router] list_of_routes = [auth_router, profile_router, account_router, keyring_router,listevents_router]
__all__ = [ __all__ = [
"list_of_routes", "list_of_routes",

View File

@ -4,24 +4,29 @@ 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 (
create_user,
get_user_accaunt_page,
get_user_by_id, get_user_by_id,
get_user_by_login,
update_user_by_id, update_user_by_id,
create_user,
get_user_by_login,
get_user_accaunt_page,
) )
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.account.account import User
from api.db.tables.account import AccountStatus
from api.schemas.base import bearer_schema from api.schemas.base import bearer_schema
from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate from api.schemas.endpoints.account import UserUpdate, AllUserResponse
from api.services.auth import get_current_user from api.services.auth import get_current_user
from api.services.update_data_validation import update_user_data_changes
from api.services.user_role_validation import db_user_role_validation from api.services.user_role_validation import db_user_role_validation
from api.services.update_data_validation import update_user_data_changes
api_router = APIRouter( api_router = APIRouter(
prefix="/account", prefix="/account",
@ -46,11 +51,9 @@ async def get_all_account(
return user_list return user_list
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate) @api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
async def get_account( async def get_account(
user_id: int, user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
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)
@ -62,27 +65,26 @@ async def get_account(
return user return user
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser) @api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
async def create_account( async def create_account(
user: UserCreate, user: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
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:
new_user = await create_user(connection, user, authorize_user.id) await create_user(connection, user, authorize_user.id)
await create_password_key(connection, user.password, new_user.id) user_new = await get_user_by_login(connection, user.login)
return new_user return user_new
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=UserUpdate) @api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
async def update_account( async def update_account(
user_id: int, user_id: int,
user_update: UserUpdate, user_update: UserUpdate,
@ -95,15 +97,12 @@ 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 = UserUpdate.model_validate({**user.model_dump(), **update_values}) user_update_data = User.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)
@ -114,9 +113,7 @@ 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, user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
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)

View File

@ -0,0 +1,174 @@
from fastapi import (
APIRouter,
Depends,
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_login
)
from api.db.logic.listevents import (
get_listevents_by_name,
get_listevents_by_id,
create_listevents,
update_listevents_by_id,
get_listevents_page,
get_listevents_page_by_creator_id
)
from api.schemas.events.list_events import ListEvent
from api.db.tables.events import EventStatus
from api.schemas.base import bearer_schema
from api.schemas.endpoints.list_events import ListEventUpdate,AllListEventResponse
from api.services.auth import get_current_user
from api.services.user_role_validation import db_user_role_validation_for_listevents_by_listevent_id,db_user_role_validation_for_listevents
from api.services.update_data_validation import update_listevents_data_changes
api_router = APIRouter(
prefix="/listevents",
tags=["list events"],
)
@api_router.get("",
dependencies=[Depends(bearer_schema)],
response_model=AllListEventResponse)
async def get_all_list_events(
page: int = 1,
limit: int = 10,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
authorize_user,page_flag = await db_user_role_validation_for_listevents(connection, current_user)
if page_flag:
list_eventslist = await get_listevents_page(connection, page, limit)
print(list_eventslist)
if list_eventslist is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
return list_eventslist
else:
list_events_list = await get_listevents_page_by_creator_id(connection,authorize_user.id, page, limit)
if list_events_list is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
return list_events_list
@api_router.get("/{listevents_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
async def get_list_events(
listevents_id: int,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user)
):
listevents_validation = await get_listevents_by_id(connection, listevents_id)
if listevents_validation is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
authorize_user = await db_user_role_validation_for_listevents_by_listevent_id(connection, current_user,listevents_validation.creator_id)
if listevents_id is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
return listevents_validation
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
async def create_list_events(
listevents: ListEventUpdate,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user)
):
user_validation = await get_user_by_login(connection, current_user)
listevents_validation = await get_listevents_by_name(connection, listevents.name)
if listevents_validation is None:
await create_listevents(connection, listevents, user_validation.id)
listevents_new = await get_listevents_by_name(connection, listevents.name)
return listevents_new
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="An List events with this information already exists."
)
@api_router.put("/{listevents_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
async def update_listevents(
listevents_id: int,
listevents_update: ListEventUpdate,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
listevents_validation = await get_listevents_by_id(connection, listevents_id)
if listevents_validation is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
authorize_user = await db_user_role_validation_for_listevents_by_listevent_id(connection, current_user,listevents_validation.creator_id)
update_values = update_listevents_data_changes(listevents_update, listevents_validation)
if update_values is None:
return listevents_validation
listevents_update_data = ListEvent.model_validate({**listevents_validation.model_dump(), **update_values})
await update_listevents_by_id(connection, update_values, listevents_validation)
listevents = await get_listevents_by_id(connection, listevents_id)
return listevents
@api_router.delete("/{listevents_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
async def delete_list_events(
listevents_id: int,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
listevents_validation = await get_listevents_by_id(connection, listevents_id)
if listevents_validation is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
authorize_user = await db_user_role_validation_for_listevents_by_listevent_id(connection, current_user,listevents_validation.creator_id)
listevents_update = ListEventUpdate(status=EventStatus.DELETED.value)
update_values = update_listevents_data_changes(listevents_update, listevents_validation)
if update_values is None:
return listevents_validation
await update_listevents_by_id(connection, update_values, listevents_validation)
listevents = await get_listevents_by_id(connection, listevents_id)
return listevents

View File

@ -1,37 +1,25 @@
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
class UserUpdate(Base): class UserUpdate(Base):
id: Optional[int] = None # id: Optional[int] = None
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
creator_id: Optional[int] = None # creator_id: Optional[int] = None
created_at: Optional[datetime] = None # created_at: Optional[datetime] = None
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
@ -47,8 +35,6 @@ 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])

View File

@ -10,8 +10,8 @@ from api.schemas.base import Base
class AccountKeyringUpdate(Base): class AccountKeyringUpdate(Base):
owner_id: Optional[int] = None owner_id: Optional[int] = None
key_type: Optional[KeyType] = None key_type: Optional[KeyType] = None
key_id: Optional[str] = Field(None, max_length=40) # key_id: Optional[str] = Field(None, max_length=40)
key_value: Optional[str] = Field(None, max_length=255) key_value: Optional[str] = Field(None, max_length=255)
created_at: Optional[datetime] = None # created_at: Optional[datetime] = None
expiry: Optional[datetime] = None # expiry: Optional[datetime] = None
status: Optional[KeyStatus] = None status: Optional[KeyStatus] = None

View File

@ -0,0 +1,38 @@
from pydantic import Field, TypeAdapter
from typing import Optional,Dict, Any, List
from datetime import datetime
from api.schemas.base import Base
from api.db.tables.events import EventState,EventStatus
class ListEventUpdate(Base):
# id: Optional[int] = None
name: Optional[str] = Field(None, max_length=40)
title: Optional[str] = Field(None, max_length=64)
# creator_id: Optional[int] = None
# created_at: Optional[datetime]= None
schema_: Optional[Dict[str, Any]]= Field(None, alias="schema")
state: Optional[EventState]= None
status: Optional[EventStatus]= None
class AllListEvent(Base):
id: int
name: str
title: str
creator_id: int
created_at: datetime
schema_: Dict[str, Any] = Field(default={}, alias="schema")
state: EventState
status: EventStatus
class AllListEventResponse(Base):
list_event: List[AllListEvent]
amount_count: int
amount_pages: int
current_page: int
limit: int
all_list_event_adapter = TypeAdapter(List[AllListEvent])

View File

@ -1,20 +1,9 @@
from pydantic import Field from pydantic import Field
from typing import Dict, Any from typing import Dict, Any
from datetime import datetime from datetime import datetime
from enum import Enum
from api.schemas.base import Base from api.schemas.base import Base
from api.db.tables.events import EventState,EventStatus
class State(Enum):
AUTO = "Auto"
DESCRIPTED = "Descripted"
class Status(Enum):
ACTIVE = "Active"
DISABLED = "Disabled"
DELETED = "Deleted"
class ListEvent(Base): class ListEvent(Base):
@ -23,6 +12,6 @@ class ListEvent(Base):
title: str = Field(..., max_length=64) title: str = Field(..., max_length=64)
creator_id: int creator_id: int
created_at: datetime created_at: datetime
schema: Dict[str, Any] schema_: Dict[str, Any] = Field(..., alias="schema")
state: State state: EventState
status: Status status: EventStatus

View File

@ -1,25 +1,27 @@
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) -> str | HTTPException: async def get_current_user(request: Request) -> Optional[User]:
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[AllUser]: async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[User]:
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

View File

@ -4,6 +4,8 @@ from api.schemas.endpoints.account import UserUpdate
from api.db.tables.account import KeyType, KeyStatus from api.db.tables.account import KeyType, KeyStatus
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
from api.db.tables.account import AccountRole, AccountStatus from api.db.tables.account import AccountRole, AccountStatus
from api.schemas.endpoints.list_events import ListEventUpdate
from api.db.tables.events import EventState, EventStatus
def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]: def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]:
@ -72,3 +74,36 @@ def update_key_data_changes(update_data: AccountKeyringUpdate, key) -> Optional[
changes[field] = new_value changes[field] = new_value
return changes if changes else None return changes if changes else None
def update_listevents_data_changes(update_data: ListEventUpdate, listevents) -> Optional[dict]:
"""
Сравнивает данные для обновления с текущими значениями listevents.
Возвращает:
- None, если нет изменений
- Словарь {поле: новое_значение} для измененных полей
"""
update_values = {}
changes = {}
for field, value in update_data.model_dump(exclude_unset=True).items():
if value is None:
continue
if isinstance(value, (EventState, EventStatus)):
update_values[field] = value.value
else:
update_values[field] = value
for field, new_value in update_values.items():
if not hasattr(listevents, field):
continue
current_value = getattr(listevents, field)
if isinstance(current_value, Enum):
current_value = current_value.value
if current_value != new_value:
changes[field] = new_value
return changes if changes else None

View File

@ -11,3 +11,17 @@ async def db_user_role_validation(connection, current_user):
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}: if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions")
return authorize_user return authorize_user
async def db_user_role_validation_for_listevents_by_listevent_id(connection, current_user,current_lisevents_creator_id):
authorize_user = await get_user_by_login(connection, current_user)
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
if authorize_user.id != current_lisevents_creator_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions")
return authorize_user
async def db_user_role_validation_for_listevents(connection, current_user):
authorize_user = await get_user_by_login(connection, current_user)
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
return authorize_user,False
else:
return authorize_user,True

View File

@ -1,6 +1,4 @@
import hashlib import hashlib
import secrets
# Хешер для работы с паролем. # Хешер для работы с паролем.
@ -16,10 +14,3 @@ 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()

View File

@ -1,23 +1,32 @@
import asyncio
import os import os
import asyncio
import hashlib
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_keyring_table, account_table, AccountRole, KeyStatus, KeyType from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
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 = hasher.generate_password() password = generate_password()
hashed_password = hasher.hash_data(password) hashed_password = hash_password(password)
create_user_query = account_table.insert().values( create_user_query = account_table.insert().values(
name=DEFAULT_LOGIN, name=DEFAULT_LOGIN,

View File

@ -1,6 +1,6 @@
[project] [project]
name = "api" name = "api"
version = "0.0.5" version = "0.0.4"
description = "" description = ""
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }] authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
readme = "README.md" readme = "README.md"

View File

@ -1,6 +1,6 @@
{ {
"name": "client", "name": "client",
"version": "0.0.5", "version": "0.0.4",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",

View File

@ -1,9 +1,10 @@
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, UserCreate, UserUpdate } from '@/types/user'; import { User } 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
@ -108,23 +109,6 @@ 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;

View File

@ -2,16 +2,12 @@ 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({
@ -19,11 +15,7 @@ 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%');
@ -38,7 +30,6 @@ 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
@ -68,21 +59,16 @@ 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={ src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
}
size={40} size={40}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
/> />
<div> <div>
<Typography.Text <Typography.Text strong style={{ display: 'block' }}>
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 }}>
{email} alexandralex@vorkout.ru
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
@ -166,7 +152,7 @@ export default function ContentDrawer({
placement="right" placement="right"
open={open} open={open}
width={width} width={width}
destroyOnHidden={true} destroyOnClose={true}
closable={false} closable={false}
> >
{children} {children}

View File

@ -1,9 +1,5 @@
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;
@ -11,13 +7,6 @@ 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={{
@ -54,24 +43,13 @@ 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://gamma.heado.ru/go/ava?name=${user?.login}`} src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`}
/> />
</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>
); );
} }

View File

@ -1,4 +1,3 @@
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';
@ -14,7 +13,6 @@ 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' }
@ -76,17 +74,15 @@ 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',
key: '/accounts', label: !collapsed ? (
label: !collapsed ? ( <Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
<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 ? (

View File

@ -8,19 +8,13 @@ import {
UploadFile, UploadFile,
GetProp, GetProp,
UploadProps, UploadProps,
message, } from 'antd';
Spin, import { useState } from 'react';
} from "antd"; import { useTranslation } from 'react-i18next';
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) => {
@ -30,17 +24,10 @@ const getBase64 = (file: FileType): Promise<string> =>
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
interface UserCreateProps { export default function UserCreate() {
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[]>([]);
@ -53,49 +40,40 @@ export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
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}
@ -107,11 +85,11 @@ export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
</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}
/> />
@ -122,24 +100,24 @@ export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
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}
@ -148,85 +126,83 @@ export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
<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: '',
bindTenantId: "", tenant: '',
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={[{ message: t("passwordMessage") }]} rules={[{ required: true, 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={[
{ message: t("emailMessage") }, { required: true, 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="bindTenantId" name="tenant"
rules={[{ message: t("tenantMessage") }]} rules={[{ required: true, 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')}>
{user && user.role === "OWNER" ? ( <Option value="Директор магазина">Директор магазина</Option>
<Option value="ADMIN">{t("ADMIN")}</Option> <Option value="Менеджер">Менеджер</Option>
) : undefined} <Option value="Кассир">Кассир</Option>
<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">{t("ACTIVE")}</Option> <Option value="ACTIVE">Активен</Option>
<Option value="DISABLED">{t("DISABLED")}</Option> <Option value="DISABLED">Неактивен</Option>
<Option value="BLOCKED">{t("BLOCKED")}</Option> <Option value="BLOCKED">Заблокирован</Option>
<Option value="DELETED">{t("DELETED")}</Option> <Option value="DELETED">Удален</Option>
</Select> </Select>
</Form.Item> </Form.Item>
@ -237,23 +213,14 @@ export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
type="primary" type="primary"
htmlType="submit" htmlType="submit"
block block
style={{ color: "#000" }} style={{ color: '#000' }}
> >
{loading ? ( <img
<> src="/icons/drawer/reg.svg"
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "} alt="save"
{t("saving")} style={{ height: '18px', width: '18px' }}
</> />{' '}
) : ( {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>

View File

@ -1,101 +1,47 @@
import { UserService } from '@/services/userService'; import { Button, Form, Input, Select } from 'antd';
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;
interface UserEditProps { export default function UserEdit() {
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={{ ...user }} initialValues={{
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={[{ message: t('nameMessage') }]} rules={[{ required: true, message: t('nameMessage') }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
{user?.id === currentUser?.id ? undefined : ( <Form.Item
<Form.Item label={t('login')}
label={t('login')} name="login"
name="login" rules={[{ required: true, message: t('loginMessage') }]}
rules={[{ message: t('loginMessage') }]} >
> <Input />
<Input /> </Form.Item>
</Form.Item>
)}
<Form.Item <Form.Item
label={t('password')} label={t('password')}
name="password" name="password"
rules={[{ message: t('passwordMessage') }]} rules={[{ required: true, message: t('passwordMessage') }]}
> >
<Input.Password /> <Input.Password />
</Form.Item> </Form.Item>
@ -113,27 +59,23 @@ export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
<Form.Item <Form.Item
label={t('tenant')} label={t('tenant')}
name="bindTenantId" name="tenant"
rules={[{ required: true, message: t('tenantMessage') }]} rules={[{ required: true, message: t('tenantMessage') }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
{user?.id === currentUser?.id ? undefined : ( <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>
{currentUser && currentUser.role === 'OWNER' ? ( <Option value="Менеджер">Менеджер</Option>
<Option value="ADMIN">{t('ADMIN')}</Option> <Option value="Кассир">Кассир</Option>
) : undefined} </Select>
<Option value="EDITOR">{t('EDITOR')}</Option> </Form.Item>
<Option value="VIEWER">{t('VIEWER')}</Option>
</Select>
</Form.Item>
)}
<Form.Item <Form.Item
label={t('status')} label={t('status')}
@ -141,10 +83,10 @@ export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
rules={[{ required: true, message: t('statusMessage') }]} rules={[{ required: true, message: t('statusMessage') }]}
> >
<Select placeholder={t('statusMessage')}> <Select placeholder={t('statusMessage')}>
<Option value="ACTIVE">{t('ACTIVE')}</Option> <Option value="ACTIVE">Активен</Option>
<Option value="DISABLED">{t('DISABLED')}</Option> <Option value="DISABLED">Неактивен</Option>
<Option value="BLOCKED">{t('BLOCKED')}</Option> <Option value="BLOCKED">Заблокирован</Option>
<Option value="DELETED">{t('DELETED')}</Option> <Option value="DELETED">Удален</Option>
</Select> </Select>
</Form.Item> </Form.Item>
@ -157,21 +99,12 @@ export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
block block
style={{ color: '#000' }} style={{ color: '#000' }}
> >
{loading ? ( <img
<> src="/icons/drawer/save.svg"
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '} alt="save"
{t('saving')} style={{ height: '18px', width: '18px' }}
</> />{' '}
) : ( {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>

View File

@ -37,20 +37,6 @@ 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: {
@ -80,20 +66,6 @@ i18n
addAccount: 'Добавить аккаунт', addAccount: 'Добавить аккаунт',
save: 'Сохранить изменения', save: 'Сохранить изменения',
newAccount: 'Новая учетная запись', newAccount: 'Новая учетная запись',
ACTIVE: 'Активен',
DISABLED: 'Выключен',
BLOCKED: 'Заблокирован',
DELETED: 'Удален',
OWNER: 'Владелец',
ADMIN: 'Админ',
EDITOR: 'Редактор',
VIEWER: 'Наблюдатель',
nameLogin: 'Имя, Логин',
createdAt: 'Создано',
saving: 'Сохранение...',
createdAccountMessage: 'Пользователь успешно создан!',
editAccountMessage: 'Пользователь успешно обновлен!',
you: '(Вы)',
}, },
}, },
}, },

View File

@ -1,219 +1,39 @@
import { useEffect, useState } from "react"; import { useState } from 'react';
import { useTranslation } from "react-i18next"; import { useTranslation } from 'react-i18next';
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user"; import { User } 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 [openCreate, setOpenCreate] = useState(false); const [open, setOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const [activeAccount, setActiveAccount] = useState< const showDrawer = () => setOpen(true);
{ login: string; id: number; name: string; email: string } | undefined const closeDrawer = () => setOpen(false);
>(undefined);
const showCreateDrawer = () => setOpenCreate(true); const [accounts, setAccounts] = useState<User[]>([]);
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={showCreateDrawer} onClick={showDrawer}
/> />
} }
/> />
<Table
size="small"
onChange={onTableChange}
columns={columns}
dataSource={accounts.users}
pagination={{
pageSize: accounts.limit,
current: accounts.currentPage,
total: accounts.amountCount,
}}
rowKey={"id"}
/>
<ContentDrawer <ContentDrawer open={open} closeDrawer={closeDrawer} type="create">
open={openCreate} <UserCreate />
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>
</> </>
); );

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Form, Input, Button, Typography, message } from 'antd'; import { Form, Input, Button, Typography } from 'antd';
import { import {
EyeInvisibleOutlined, EyeInvisibleOutlined,
EyeTwoTone, EyeTwoTone,
@ -45,11 +45,7 @@ export default function LoginPage() {
/> />
</div> </div>
<Form <Form name="login" onFinish={onFinish} layout="vertical">
name="login"
onFinish={onFinish}
layout="vertical"
>
<Form.Item <Form.Item
name="login" name="login"
rules={[{ required: true, message: 'Введите login' }]} rules={[{ required: true, message: 'Введите login' }]}

View File

@ -1,5 +1,5 @@
import api from '@/api/api'; import api from '@/api/api';
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user'; import { User } from '@/types/user';
export class UserService { export class UserService {
static async getProfile(): Promise<User> { static async getProfile(): Promise<User> {
@ -9,30 +9,8 @@ export class UserService {
return user; return user;
} }
static async getUsers( static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
page: number = 1, const users = api.getUsers(page, limit);
limit: number = 10 return users;
): 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;
} }
} }

View File

@ -191,10 +191,6 @@ export interface components {
amountCount: number; amountCount: number;
/** Amountpages */ /** Amountpages */
amountPages: number; amountPages: number;
/** Currentpage */
currentPage: number;
/** Limit */
limit: number;
}; };
/** Auth */ /** Auth */
Auth: { Auth: {
@ -251,25 +247,6 @@ 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 */
@ -280,8 +257,6 @@ 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;
@ -460,7 +435,7 @@ export interface operations {
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["UserCreate"]; "application/json": components["schemas"]["UserUpdate"];
}; };
}; };
responses: { responses: {
@ -470,7 +445,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["AllUser"]; "application/json": components["schemas"]["User"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */
@ -501,7 +476,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["UserUpdate"]; "application/json": components["schemas"]["User"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */
@ -536,7 +511,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["UserUpdate"]; "application/json": components["schemas"]["User"];
}; };
}; };
/** @description Validation Error */ /** @description Validation Error */

View File

@ -1,9 +1,3 @@
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'];