Compare commits
81 Commits
VORKOUT-6-
...
VORKOUT-16
Author | SHA1 | Date | |
---|---|---|---|
6db2821781 | |||
3f87970653 | |||
ee4051f523 | |||
cf6e09d8c1 | |||
|
d9e88d66f7 | ||
|
3c300f7c7a | ||
|
c66c21eb14 | ||
|
a9ecaadad6 | ||
|
e50bb32470 | ||
|
2030d54b2c | ||
|
b82960faf3 | ||
|
e0887c240f | ||
65ed6b9561 | |||
b503bc0bef | |||
5cdb6e460f | |||
5966b7d6f1 | |||
8b0659fc89 | |||
51344ef218 | |||
51171377e2 | |||
943f4ded2d | |||
443293d420 | |||
91c8efe13f | |||
5e3c3b4672 | |||
60160d46be | |||
d03600b23d | |||
82fcd22faf | |||
|
2690843954 | ||
e10430310f | |||
2493cd0d9f | |||
e6589a0950 | |||
a0fcc324fa | |||
2a35386c1d | |||
|
f65f4caf5c | ||
|
a90a802659 | ||
|
3d5d717962 | ||
|
0bff556b10 | ||
a936500101 | |||
4c0beb24f9 | |||
ad1369c3e3 | |||
|
f550b86c68 | ||
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 | |||
92ff1d3d0a | |||
5ed8ca9251 | |||
8ac329e76e | |||
c68b512902 | |||
70aaaeabf1 | |||
92203351ff | |||
ee92428ec3 | |||
c87581c9e2 | |||
79cb434ebd | |||
5def1a9bb1 | |||
d55a99aafd | |||
599bf22bda | |||
e114f963ab |
24
Makefile
24
Makefile
@@ -12,36 +12,35 @@ 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 && \
|
||||||
npm start
|
npm run dev
|
||||||
|
|
||||||
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:
|
||||||
|
cd client && \
|
||||||
|
npm install
|
||||||
|
|
||||||
install:
|
install:
|
||||||
make migrate head && \
|
make migrate head && \
|
||||||
cd api && \
|
cd api && \
|
||||||
@@ -58,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
|
||||||
|
50
README.md
50
README.md
@@ -1 +1,49 @@
|
|||||||
Vorkout/connect
|
# Vorkout/connect
|
||||||
|
|
||||||
|
### Makefile cheat sheet
|
||||||
|
|
||||||
|
```Makefile
|
||||||
|
Dev:
|
||||||
|
venv-api create python virtual environment
|
||||||
|
venv-client install node modules
|
||||||
|
install Migrate database and initialize project
|
||||||
|
|
||||||
|
Application Api:
|
||||||
|
start-api Run api server
|
||||||
|
|
||||||
|
Application Client:
|
||||||
|
start-client Run client server
|
||||||
|
|
||||||
|
Prod:
|
||||||
|
...
|
||||||
|
|
||||||
|
Code:
|
||||||
|
check-api Check api code with ruff
|
||||||
|
format-api Reformat api code with ruff
|
||||||
|
|
||||||
|
Help:
|
||||||
|
...
|
||||||
|
|
||||||
|
Testing:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск в режиме разработки
|
||||||
|
|
||||||
|
Для запуска в режиме разработки нужно
|
||||||
|
|
||||||
|
1. Устрановить среду для clint и api
|
||||||
|
2. Запустить в докере или локально необходимые сервисы (базуб брокер и redis) `make services`
|
||||||
|
3. Для миграции и создания первого пользователя необходимо запустить `make install`
|
||||||
|
3. Запустить api `make start-api`
|
||||||
|
4. Запустить client `make start-client`
|
||||||
|
|
||||||
|
### Миграции алембик
|
||||||
|
|
||||||
|
1. Стоит внимательно учитывать, адрес какой базы стоит в настройках alembic - локальной или продакшн. Посмотреть это можно в файле [env.py](connect/api/api/db/alembic/env.py). Конфиг для локальной базы
|
||||||
|
```python
|
||||||
|
config.set_main_option(
|
||||||
|
"sqlalchemy.url",
|
||||||
|
f"mysql+pymysql://root:hackme@localhost:3306/connect_test",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
@@ -73,6 +73,7 @@ if __name__ == "__main__":
|
|||||||
log_level="info",
|
log_level="info",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_middleware(MiddlewareAccessTokenValidadtion)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=origins,
|
||||||
@@ -80,5 +81,3 @@ app.add_middleware(
|
|||||||
allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"],
|
allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(MiddlewareAccessTokenValidadtion)
|
|
||||||
|
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,77 +1,136 @@
|
|||||||
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 insert, select, func, or_, and_, asc, desc
|
||||||
|
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, UserFilterDTO
|
||||||
|
|
||||||
|
|
||||||
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[User]:
|
async def get_user_account_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: UserFilterDTO
|
||||||
|
) -> Optional[AllUserResponse]:
|
||||||
"""
|
"""
|
||||||
Получает список ползовелей заданных значениями page, limit.
|
Получает список пользователей с пагинацией, фильтрацией и сортировкой через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- поиск
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
"""
|
"""
|
||||||
|
|
||||||
first_user = page * limit - (limit)
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
query = (
|
query = select(
|
||||||
select(
|
|
||||||
account_table.c.id,
|
account_table.c.id,
|
||||||
account_table.c.name,
|
account_table.c.name,
|
||||||
account_table.c.login,
|
account_table.c.login,
|
||||||
account_table.c.email,
|
account_table.c.email,
|
||||||
account_table.c.bind_tenant_id,
|
account_table.c.bind_tenant_id,
|
||||||
account_table.c.role,
|
account_table.c.role,
|
||||||
|
account_table.c.meta,
|
||||||
|
account_table.c.creator_id,
|
||||||
account_table.c.created_at,
|
account_table.c.created_at,
|
||||||
account_table.c.status,
|
account_table.c.status,
|
||||||
)
|
)
|
||||||
.order_by(account_table.c.id)
|
|
||||||
.offset(first_user)
|
# Поиск
|
||||||
.limit(limit)
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
query = query.where(
|
||||||
|
or_(
|
||||||
|
account_table.c.name.ilike(search_term),
|
||||||
|
account_table.c.login.ilike(search_term),
|
||||||
|
account_table.c.email.ilike(search_term),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Фильтрацию
|
||||||
|
filter_conditions = []
|
||||||
|
if filter_dto.filters:
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(account_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
# Сортировка
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
column = getattr(account_table.c, order_field, None)
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(account_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
count_query = select(func.count()).select_from(account_table)
|
count_query = select(func.count()).select_from(account_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(
|
||||||
|
account_table.c.name.ilike(search_term),
|
||||||
|
account_table.c.login.ilike(search_term),
|
||||||
|
account_table.c.email.ilike(search_term),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
result = await connection.execute(query)
|
result = await connection.execute(query)
|
||||||
count_result = await connection.execute(count_query)
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
users_data = result.mappings().all()
|
users_data = result.mappings().all()
|
||||||
total_count = count_result.scalar()
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
total_pages = math.ceil(total_count / limit)
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
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 User.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]:
|
||||||
@@ -81,20 +140,10 @@ async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional
|
|||||||
query = select(account_table).where(account_table.c.login == login)
|
query = select(account_table).where(account_table.c.login == login)
|
||||||
|
|
||||||
user_db_cursor = await connection.execute(query)
|
user_db_cursor = await connection.execute(query)
|
||||||
user_db = user_db_cursor.one_or_none()
|
user_data = user_db_cursor.mappings().one_or_none()
|
||||||
|
if not user_data:
|
||||||
if not user_db:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user_data = {
|
|
||||||
column.name: (
|
|
||||||
getattr(user_db, column.name).name
|
|
||||||
if isinstance(getattr(user_db, column.name), Enum)
|
|
||||||
else getattr(user_db, column.name)
|
|
||||||
)
|
|
||||||
for column in account_table.columns
|
|
||||||
}
|
|
||||||
|
|
||||||
return User.model_validate(user_data)
|
return User.model_validate(user_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -107,7 +156,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 +166,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,18 +46,17 @@ 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
|
||||||
|
|
||||||
|
|
||||||
async def upgrade_old_refresh_token(connection: AsyncConnection, user, refresh_token) -> Optional[User]:
|
async def upgrade_old_refresh_token(connection: AsyncConnection, refresh_token) -> Optional[User]:
|
||||||
new_status = KeyStatus.EXPIRED
|
new_status = KeyStatus.EXPIRED
|
||||||
|
|
||||||
update_query = (
|
update_query = (
|
||||||
update(account_keyring_table)
|
update(account_keyring_table)
|
||||||
.where(
|
.where(
|
||||||
account_table.c.id == user.id,
|
|
||||||
account_keyring_table.c.status == KeyStatus.ACTIVE,
|
account_keyring_table.c.status == KeyStatus.ACTIVE,
|
||||||
account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN,
|
account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN,
|
||||||
account_keyring_table.c.key_value == refresh_token,
|
account_keyring_table.c.key_value == refresh_token,
|
||||||
|
@@ -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]:
|
||||||
@@ -17,20 +18,11 @@ async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[Ac
|
|||||||
query = select(account_keyring_table).where(account_keyring_table.c.key_id == key_id)
|
query = select(account_keyring_table).where(account_keyring_table.c.key_id == key_id)
|
||||||
|
|
||||||
user_db_cursor = await connection.execute(query)
|
user_db_cursor = await connection.execute(query)
|
||||||
user_db = user_db_cursor.one_or_none()
|
|
||||||
|
|
||||||
if not user_db:
|
user_data = user_db_cursor.mappings().one_or_none()
|
||||||
|
if not user_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user_data = {
|
|
||||||
column.name: (
|
|
||||||
getattr(user_db, column.name).name
|
|
||||||
if isinstance(getattr(user_db, column.name), Enum)
|
|
||||||
else getattr(user_db, column.name)
|
|
||||||
)
|
|
||||||
for column in account_keyring_table.columns
|
|
||||||
}
|
|
||||||
|
|
||||||
return AccountKeyring.model_validate(user_data)
|
return AccountKeyring.model_validate(user_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,3 +59,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()
|
||||||
|
279
api/api/db/logic/list_events.py
Normal file
279
api/api/db/logic/list_events.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
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, ListEventFilterDTO
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: ListEventFilterDTO
|
||||||
|
) -> Optional[AllListEventResponse]:
|
||||||
|
"""
|
||||||
|
Получает список событий с фильтрацией через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- полнотекстовый поиск (пропускает name при русских буквах)
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
|
"""
|
||||||
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * 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.label("schema_"),
|
||||||
|
list_events_table.c.state,
|
||||||
|
list_events_table.c.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
has_russian = any("\u0400" <= char <= "\u04ff" for char in filter_dto.search)
|
||||||
|
|
||||||
|
if has_russian:
|
||||||
|
query = query.where(list_events_table.c.title.ilike(search_term))
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
or_(list_events_table.c.title.ilike(search_term), list_events_table.c.name.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
filter_conditions = []
|
||||||
|
if filter_dto.filters:
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(list_events_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
if order_field.startswith("schema."):
|
||||||
|
json_field = order_field[7:]
|
||||||
|
column = list_events_table.c.schema[json_field].astext
|
||||||
|
else:
|
||||||
|
column = getattr(list_events_table.c, order_field, None)
|
||||||
|
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(list_events_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(list_events_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
has_russian = any("\u0400" <= char <= "\u04ff" for char in filter_dto.search)
|
||||||
|
|
||||||
|
if has_russian:
|
||||||
|
count_query = count_query.where(list_events_table.c.title.ilike(search_term))
|
||||||
|
else:
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(list_events_table.c.title.ilike(search_term), list_events_table.c.name.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
validated_events = all_list_event_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllListEventResponse(
|
||||||
|
list_event=validated_events,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_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_list_events_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_list_events_by_name(connection: AsyncConnection, name: str) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Получает list events по name.
|
||||||
|
"""
|
||||||
|
query = select(list_events_table).where(list_events_table.c.name == name)
|
||||||
|
|
||||||
|
list_events_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
list_events_data = list_events_db_cursor.mappings().one_or_none()
|
||||||
|
if not list_events_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ListEvent.model_validate(list_events_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_by_id(connection: AsyncConnection, id: int) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Получает listevent по id.
|
||||||
|
"""
|
||||||
|
query = select(list_events_table).where(list_events_table.c.id == id)
|
||||||
|
|
||||||
|
list_events_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
list_events_data = list_events_db_cursor.mappings().one_or_none()
|
||||||
|
if not list_events_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ListEvent.model_validate(list_events_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_list_events_by_id(connection: AsyncConnection, update_values, list_events):
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы list_events_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(
|
||||||
|
list_events_table.update().where(list_events_table.c.id == list_events.id).values(**update_values)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_list_events(
|
||||||
|
connection: AsyncConnection, list_events: ListEvent, creator_id: int
|
||||||
|
) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице list_events_table.
|
||||||
|
"""
|
||||||
|
query = insert(list_events_table).values(
|
||||||
|
name=list_events.name,
|
||||||
|
title=list_events.title, # добавлено поле title
|
||||||
|
creator_id=creator_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
schema=list_events.schema_, # добавлено поле schema
|
||||||
|
state=list_events.state.value, # добавлено поле state
|
||||||
|
status=list_events.status.value, # добавлено поле status
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
return list_events
|
175
api/api/db/logic/process_schema.py
Normal file
175
api/api/db/logic/process_schema.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.tables.process import process_schema_table
|
||||||
|
|
||||||
|
from api.schemas.process.process_schema import ProcessSchema
|
||||||
|
|
||||||
|
from api.schemas.endpoints.process_schema import (
|
||||||
|
all_process_schema_adapter,
|
||||||
|
AllProcessSchemaResponse,
|
||||||
|
ProcessSchemaFilterDTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: ProcessSchemaFilterDTO
|
||||||
|
) -> Optional[AllProcessSchemaResponse]:
|
||||||
|
"""
|
||||||
|
Получает список схем процессов с комплексной фильтрацией через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- поиск
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
|
"""
|
||||||
|
|
||||||
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
process_schema_table.c.id,
|
||||||
|
process_schema_table.c.title,
|
||||||
|
process_schema_table.c.description,
|
||||||
|
process_schema_table.c.owner_id,
|
||||||
|
process_schema_table.c.creator_id,
|
||||||
|
process_schema_table.c.created_at,
|
||||||
|
process_schema_table.c.settings,
|
||||||
|
process_schema_table.c.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
query = query.where(
|
||||||
|
or_(process_schema_table.c.title.ilike(search_term), process_schema_table.c.description.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.filters:
|
||||||
|
filter_conditions = []
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(process_schema_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
column = getattr(process_schema_table.c, order_field, None)
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(process_schema_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(process_schema_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(process_schema_table.c.title.ilike(search_term), process_schema_table.c.description.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.filters and filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
|
validated_process_schema = all_process_schema_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllProcessSchemaResponse(
|
||||||
|
process_schema=validated_process_schema,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_by_title(connection: AsyncConnection, title: str) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Получает process schema по title.
|
||||||
|
"""
|
||||||
|
query = select(process_schema_table).where(process_schema_table.c.title == title)
|
||||||
|
|
||||||
|
process_schema_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
process_schema_data = process_schema_db_cursor.mappings().one_or_none()
|
||||||
|
if not process_schema_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ProcessSchema.model_validate(process_schema_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_by_id(connection: AsyncConnection, id: int) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Получает process_schema по id.
|
||||||
|
"""
|
||||||
|
query = select(process_schema_table).where(process_schema_table.c.id == id)
|
||||||
|
|
||||||
|
process_schema_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
process_schema_data = process_schema_db_cursor.mappings().one_or_none()
|
||||||
|
if not process_schema_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ProcessSchema.model_validate(process_schema_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_process_schema_by_id(connection: AsyncConnection, update_values, process_schema):
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы process_schema_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(
|
||||||
|
process_schema_table.update().where(process_schema_table.c.id == process_schema.id).values(**update_values)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_process_schema(
|
||||||
|
connection: AsyncConnection, process_schema: ProcessSchema, creator_id: int
|
||||||
|
) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице process_schema_table.
|
||||||
|
"""
|
||||||
|
query = insert(process_schema_table).values(
|
||||||
|
title=process_schema.title,
|
||||||
|
description=process_schema.description,
|
||||||
|
owner_id=process_schema.owner_id,
|
||||||
|
creator_id=creator_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
settings=process_schema.settings,
|
||||||
|
status=process_schema.status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
return process_schema
|
@@ -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(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),
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import enum
|
import enum
|
||||||
|
|
||||||
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
|
from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
from api.db.sql_types import UnsignedInt
|
from api.db.sql_types import UnsignedInt
|
||||||
|
|
||||||
|
@@ -3,7 +3,6 @@ import enum
|
|||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Table,
|
Table,
|
||||||
Column,
|
Column,
|
||||||
Integer,
|
|
||||||
String,
|
String,
|
||||||
Text,
|
Text,
|
||||||
Enum as SQLAEnum,
|
Enum as SQLAEnum,
|
||||||
@@ -14,7 +13,7 @@ from sqlalchemy import (
|
|||||||
PrimaryKeyConstraint,
|
PrimaryKeyConstraint,
|
||||||
)
|
)
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from enum import Enum, auto
|
from enum import Enum
|
||||||
|
|
||||||
from api.db.sql_types import UnsignedInt
|
from api.db.sql_types import UnsignedInt
|
||||||
|
|
||||||
|
@@ -2,8 +2,10 @@ 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.list_events import api_router as listevents_router
|
||||||
|
from api.endpoints.process_schema import api_router as processschema_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, processschema_router]
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"list_of_routes",
|
"list_of_routes",
|
||||||
|
@@ -1,32 +1,23 @@
|
|||||||
from fastapi import (
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
APIRouter,
|
|
||||||
Depends,
|
|
||||||
HTTPException,
|
|
||||||
status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
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_account_page_DTO,
|
||||||
|
get_user_by_id,
|
||||||
get_user_by_login,
|
get_user_by_login,
|
||||||
get_user_accaunt_page,
|
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 AllUserResponse, UserCreate, UserUpdate, UserFilterDTO
|
||||||
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.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",
|
||||||
@@ -36,14 +27,33 @@ api_router = APIRouter(
|
|||||||
|
|
||||||
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllUserResponse)
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllUserResponse)
|
||||||
async def get_all_account(
|
async def get_all_account(
|
||||||
page: int = 1,
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
limit: int = 10,
|
limit: int = Query(10, description="КNumber of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by name or login or email"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
role_filter: Optional[List[str]] = Query(None, description="Filter by role"),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
connection: AsyncConnection = Depends(get_connection_dep),
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
current_user=Depends(get_current_user),
|
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_list = await get_user_accaunt_page(connection, page, limit)
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"role": role_filter} if role_filter else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = UserFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_list = await get_user_account_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
if user_list is None:
|
if user_list is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Accounts not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Accounts not found")
|
||||||
@@ -53,7 +63,9 @@ async def get_all_account(
|
|||||||
|
|
||||||
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
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)
|
||||||
|
|
||||||
@@ -67,24 +79,25 @@ async def get_account(
|
|||||||
|
|
||||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
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,14 +110,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")
|
||||||
|
|
||||||
update_values = update_user_data_changes(user_update, user)
|
if user_update.password is not None:
|
||||||
|
await update_password_key(connection, user.id, user_update.password)
|
||||||
|
|
||||||
if update_values is None:
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
user_update_data = User.model_validate({**user.model_dump(), **update_values})
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
await update_user_by_id(connection, update_values, user)
|
|
||||||
|
|
||||||
user = await get_user_by_id(connection, user_id)
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
|
||||||
@@ -113,7 +127,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)
|
||||||
|
|
||||||
@@ -123,12 +139,12 @@ async def delete_account(
|
|||||||
|
|
||||||
user_update = UserUpdate(status=AccountStatus.DELETED.value)
|
user_update = UserUpdate(status=AccountStatus.DELETED.value)
|
||||||
|
|
||||||
update_values = update_user_data_changes(user_update, user)
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
if update_values is None:
|
if not updated_values:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
await update_user_by_id(connection, update_values, user)
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
user = await get_user_by_id(connection, user_id)
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
|
||||||
|
@@ -4,9 +4,9 @@ from fastapi import (
|
|||||||
APIRouter,
|
APIRouter,
|
||||||
Depends,
|
Depends,
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Request,
|
|
||||||
Response,
|
Response,
|
||||||
status,
|
status,
|
||||||
|
Request,
|
||||||
)
|
)
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -22,7 +22,7 @@ from api.services.auth import authenticate_user
|
|||||||
|
|
||||||
from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token
|
from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token
|
||||||
|
|
||||||
from api.schemas.endpoints.auth import Auth, Access
|
from api.schemas.endpoints.auth import Auth, Tokens
|
||||||
|
|
||||||
api_router = APIRouter(
|
api_router = APIRouter(
|
||||||
prefix="/auth",
|
prefix="/auth",
|
||||||
@@ -33,7 +33,7 @@ api_router = APIRouter(
|
|||||||
class Settings(BaseModel):
|
class Settings(BaseModel):
|
||||||
authjwt_secret_key: str = get_settings().SECRET_KEY
|
authjwt_secret_key: str = get_settings().SECRET_KEY
|
||||||
# Configure application to store and get JWT from cookies
|
# Configure application to store and get JWT from cookies
|
||||||
authjwt_token_location: set = {"headers", "cookies"}
|
authjwt_token_location: set = {"headers"}
|
||||||
authjwt_cookie_domain: str = get_settings().DOMAIN
|
authjwt_cookie_domain: str = get_settings().DOMAIN
|
||||||
|
|
||||||
# Only allow JWT cookies to be sent over https
|
# Only allow JWT cookies to be sent over https
|
||||||
@@ -48,7 +48,7 @@ def get_config():
|
|||||||
return Settings()
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("", response_model=Access)
|
@api_router.post("", response_model=Tokens)
|
||||||
async def login_for_access_token(
|
async def login_for_access_token(
|
||||||
user: Auth,
|
user: Auth,
|
||||||
response: Response,
|
response: Response,
|
||||||
@@ -69,7 +69,6 @@ async def login_for_access_token(
|
|||||||
)
|
)
|
||||||
|
|
||||||
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
|
||||||
refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS)
|
refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS)
|
||||||
|
|
||||||
logger.debug(f"refresh_token_expires {refresh_token_expires}")
|
logger.debug(f"refresh_token_expires {refresh_token_expires}")
|
||||||
@@ -81,35 +80,27 @@ async def login_for_access_token(
|
|||||||
|
|
||||||
await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user)
|
await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user)
|
||||||
|
|
||||||
Authorize.set_refresh_cookies(refresh_token)
|
return Tokens(access_token=access_token, refresh_token=refresh_token)
|
||||||
|
|
||||||
return Access(access_token=access_token)
|
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/refresh", response_model=Access)
|
@api_router.post("/refresh", response_model=Tokens)
|
||||||
async def refresh(
|
async def refresh(
|
||||||
request: Request, connection: AsyncConnection = Depends(get_connection_dep), Authorize: AuthJWT = Depends()
|
request: Request,
|
||||||
):
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
refresh_token = request.cookies.get("refresh_token_cookie")
|
Authorize: AuthJWT = Depends(),
|
||||||
# print("Refresh Token:", refresh_token)
|
) -> Tokens:
|
||||||
|
|
||||||
if not refresh_token:
|
|
||||||
raise HTTPException(status_code=401, detail="Refresh token is missing")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Authorize.jwt_refresh_token_required()
|
Authorize.jwt_refresh_token_required()
|
||||||
current_user = Authorize.get_jwt_subject()
|
current_user = Authorize.get_jwt_subject()
|
||||||
|
except Exception:
|
||||||
except Exception as e:
|
refresh_token = request.headers.get("Authorization").split(" ")[1]
|
||||||
await upgrade_old_refresh_token(connection, current_user, refresh_token)
|
await upgrade_old_refresh_token(connection, refresh_token)
|
||||||
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid refresh token",
|
detail="Invalid refresh token",
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
|
||||||
new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires)
|
new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires)
|
||||||
|
|
||||||
return Access(access_token=new_access_token)
|
return Tokens(access_token=new_access_token)
|
||||||
|
@@ -24,7 +24,6 @@ from api.schemas.account.account_keyring import AccountKeyring
|
|||||||
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.user_role_validation import db_user_role_validation
|
||||||
from api.services.update_data_validation import update_key_data_changes
|
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter(
|
api_router = APIRouter(
|
||||||
@@ -87,14 +86,12 @@ async def update_keyring(
|
|||||||
if keyring is None:
|
if keyring is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
|
||||||
|
|
||||||
update_values = update_key_data_changes(keyring_update, keyring)
|
updated_values = keyring_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
if update_values is None:
|
if not updated_values:
|
||||||
return keyring
|
return keyring
|
||||||
|
|
||||||
keyring_update_data = AccountKeyring.model_validate({**keyring.model_dump(), **update_values})
|
await update_key_by_id(connection, updated_values, keyring)
|
||||||
|
|
||||||
await update_key_by_id(connection, update_values, keyring)
|
|
||||||
|
|
||||||
keyring = await get_key_by_id(connection, key_id)
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
@@ -116,12 +113,12 @@ async def delete_keyring(
|
|||||||
|
|
||||||
keyring_update = AccountKeyringUpdate(status=KeyStatus.DELETED.value)
|
keyring_update = AccountKeyringUpdate(status=KeyStatus.DELETED.value)
|
||||||
|
|
||||||
update_values = update_key_data_changes(keyring_update, keyring)
|
updated_values = keyring_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
if update_values is None:
|
if not updated_values:
|
||||||
return keyring
|
return keyring
|
||||||
|
|
||||||
await update_key_by_id(connection, update_values, keyring)
|
await update_key_by_id(connection, updated_values, keyring)
|
||||||
|
|
||||||
keyring = await get_key_by_id(connection, key_id)
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
|
179
api/api/endpoints/list_events.py
Normal file
179
api/api/endpoints/list_events.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
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.list_events import (
|
||||||
|
get_list_events_by_name,
|
||||||
|
get_list_events_by_id,
|
||||||
|
create_list_events,
|
||||||
|
update_list_events_by_id,
|
||||||
|
get_list_events_page_DTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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, ListEventFilterDTO
|
||||||
|
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
|
||||||
|
from api.services.user_role_validation import (
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/list_events",
|
||||||
|
tags=["list events"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllListEventResponse)
|
||||||
|
async def get_all_list_events(
|
||||||
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
|
limit: int = Query(10, description="Number of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by title or name"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
state_filter: Optional[List[str]] = Query(None, description="Filter by state"),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"state": state_filter} if state_filter else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = ListEventFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
authorize_user, page_flag = await db_user_role_validation_for_list_events_and_process_schema(
|
||||||
|
connection, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not page_flag:
|
||||||
|
if filter_dto.filters is None:
|
||||||
|
filter_dto.filters = {}
|
||||||
|
filter_dto.filters["creator_id"] = [str(authorize_user.id)]
|
||||||
|
|
||||||
|
list_events_page = await get_list_events_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
|
if list_events_page is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
return list_events_page
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def get_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_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_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if list_events_id is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def create_list_events(
|
||||||
|
list_events: ListEventUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user_validation = await get_user_by_login(connection, current_user)
|
||||||
|
list_events_validation = await get_list_events_by_name(connection, list_events.name)
|
||||||
|
|
||||||
|
if list_events_validation is None:
|
||||||
|
await create_list_events(connection, list_events, user_validation.id)
|
||||||
|
list_events_new = await get_list_events_by_name(connection, list_events.name)
|
||||||
|
return list_events_new
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An List events with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def update_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
list_events_update: ListEventUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_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_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_values = list_events_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
await update_list_events_by_id(connection, updated_values, list_events_validation)
|
||||||
|
|
||||||
|
list_events = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
return list_events
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def delete_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_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_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
list_events_update = ListEventUpdate(status=EventStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = list_events_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
await update_list_events_by_id(connection, updated_values, list_events_validation)
|
||||||
|
|
||||||
|
list_events = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
return list_events
|
178
api/api/endpoints/process_schema.py
Normal file
178
api/api/endpoints/process_schema.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
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.process_schema import (
|
||||||
|
get_process_schema_by_title,
|
||||||
|
create_process_schema,
|
||||||
|
get_process_schema_by_id,
|
||||||
|
update_process_schema_by_id,
|
||||||
|
get_process_schema_page_DTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
from api.schemas.process.process_schema import ProcessSchema
|
||||||
|
|
||||||
|
from api.db.tables.process import ProcessStatus
|
||||||
|
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
|
||||||
|
from api.schemas.endpoints.process_schema import ProcessSchemaUpdate, AllProcessSchemaResponse, ProcessSchemaFilterDTO
|
||||||
|
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
|
||||||
|
from api.services.user_role_validation import (
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/process_schema",
|
||||||
|
tags=["process schema"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllProcessSchemaResponse)
|
||||||
|
async def get_all_process_schema(
|
||||||
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
|
limit: int = Query(10, description="Number of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by title or description"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
owner_id: Optional[List[str]] = Query(None, description="Filter by owner id"),
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"owner_id": owner_id} if owner_id else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = ProcessSchemaFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
authorize_user, page_flag = await db_user_role_validation_for_list_events_and_process_schema(
|
||||||
|
connection, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not page_flag:
|
||||||
|
if filter_dto.filters is None:
|
||||||
|
filter_dto.filters = {}
|
||||||
|
filter_dto.filters["creator_id"] = [str(authorize_user.id)]
|
||||||
|
|
||||||
|
process_schema_page = await get_process_schema_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
|
if process_schema_page is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
return process_schema_page
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def get_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if process_schema_id is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def create_processschema(
|
||||||
|
process_schema: ProcessSchemaUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user_validation = await get_user_by_login(connection, current_user)
|
||||||
|
process_schema_validation = await get_process_schema_by_title(connection, process_schema.title)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
await create_process_schema(connection, process_schema, user_validation.id)
|
||||||
|
process_schema_new = await get_process_schema_by_title(connection, process_schema.title)
|
||||||
|
return process_schema_new
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An process schema with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def update_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
process_schema_update: ProcessSchemaUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_values = process_schema_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
await update_process_schema_by_id(connection, updated_values, process_schema_validation)
|
||||||
|
|
||||||
|
process_schema = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
return process_schema
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def delete_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
process_schema_update = ProcessSchemaUpdate(status=ProcessStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = process_schema_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
await update_process_schema_by_id(connection, updated_values, process_schema_validation)
|
||||||
|
|
||||||
|
process_schema = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
return process_schema
|
@@ -1,11 +1,7 @@
|
|||||||
from fastapi import (
|
from fastapi import (
|
||||||
APIRouter,
|
APIRouter,
|
||||||
Body,
|
|
||||||
Depends,
|
Depends,
|
||||||
Form,
|
|
||||||
HTTPException,
|
HTTPException,
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +12,6 @@ from api.db.connection.session import get_connection_dep
|
|||||||
from api.db.logic.account import get_user_by_id, update_user_by_id, get_user_by_login
|
from api.db.logic.account import get_user_by_id, update_user_by_id, get_user_by_login
|
||||||
from api.schemas.base import bearer_schema
|
from api.schemas.base import bearer_schema
|
||||||
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.schemas.endpoints.account import UserUpdate
|
from api.schemas.endpoints.account import UserUpdate
|
||||||
from api.schemas.account.account import User
|
from api.schemas.account.account import User
|
||||||
@@ -42,7 +37,7 @@ async def get_profile(
|
|||||||
|
|
||||||
@api_router.put("", dependencies=[Depends(bearer_schema)], response_model=User)
|
@api_router.put("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
async def update_profile(
|
async def update_profile(
|
||||||
user_updata: UserUpdate,
|
user_update: UserUpdate,
|
||||||
connection: AsyncConnection = Depends(get_connection_dep),
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
current_user=Depends(get_current_user),
|
current_user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
@@ -50,13 +45,13 @@ async def update_profile(
|
|||||||
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_updata.role == None and user_updata.login == None:
|
if user_update.role is None and user_update.login is None:
|
||||||
update_values = update_user_data_changes(user_updata, user)
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
if update_values is None:
|
if updated_values is None:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
await update_user_by_id(connection, update_values, user)
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
user = await get_user_by_id(connection, user.id)
|
user = await get_user_by_id(connection, user.id)
|
||||||
|
|
||||||
|
@@ -1,25 +1,34 @@
|
|||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
||||||
@@ -27,6 +36,8 @@ class AllUser(Base):
|
|||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
bind_tenant_id: Optional[str] = None
|
bind_tenant_id: Optional[str] = None
|
||||||
role: AccountRole
|
role: AccountRole
|
||||||
|
meta: Optional[dict] = None
|
||||||
|
creator_id: Optional[int] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
status: AccountStatus
|
status: AccountStatus
|
||||||
|
|
||||||
@@ -35,6 +46,15 @@ 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])
|
||||||
|
|
||||||
|
|
||||||
|
class UserFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
||||||
|
@@ -1,17 +1,11 @@
|
|||||||
import datetime
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from datetime import datetime
|
|
||||||
from api.db.tables.account import KeyType, KeyStatus
|
from api.db.tables.account import KeyType, KeyStatus
|
||||||
|
|
||||||
from api.schemas.base import Base
|
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_value: Optional[str] = Field(None, max_length=255)
|
key_value: Optional[str] = Field(None, max_length=255)
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
expiry: Optional[datetime] = None
|
|
||||||
status: Optional[KeyStatus] = None
|
status: Optional[KeyStatus] = None
|
||||||
|
@@ -8,9 +8,6 @@ class Auth(Base):
|
|||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class Refresh(Base):
|
class Tokens(Base):
|
||||||
refresh_token: str
|
|
||||||
|
|
||||||
|
|
||||||
class Access(Base):
|
|
||||||
access_token: str
|
access_token: str
|
||||||
|
refresh_token: str | None = None
|
||||||
|
44
api/api/schemas/endpoints/list_events.py
Normal file
44
api/api/schemas/endpoints/list_events.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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):
|
||||||
|
name: Optional[str] = Field(None, max_length=40)
|
||||||
|
title: Optional[str] = Field(None, max_length=64)
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
class ListEventFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
44
api/api/schemas/endpoints/process_schema.py
Normal file
44
api/api/schemas/endpoints/process_schema.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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.process import ProcessStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSchemaUpdate(Base):
|
||||||
|
title: Optional[str] = Field(None, max_length=100)
|
||||||
|
description: Optional[str] = None
|
||||||
|
owner_id: Optional[int] = None
|
||||||
|
settings: Optional[Dict[str, Any]] = None
|
||||||
|
status: Optional[ProcessStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AllProcessSchema(Base):
|
||||||
|
id: int
|
||||||
|
title: str = Field(..., max_length=100)
|
||||||
|
description: str
|
||||||
|
owner_id: int
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
settings: Dict[str, Any]
|
||||||
|
status: ProcessStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AllProcessSchemaResponse(Base):
|
||||||
|
process_schema: List[AllProcessSchema]
|
||||||
|
amount_count: int
|
||||||
|
amount_pages: int
|
||||||
|
current_page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
all_process_schema_adapter = TypeAdapter(List[AllProcessSchema])
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSchemaFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
@@ -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
|
||||||
|
@@ -1,16 +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.process import NodeStatus
|
||||||
|
|
||||||
class Status(Enum):
|
|
||||||
ACTIVE = "Active"
|
|
||||||
STOPPING = "Stopping"
|
|
||||||
STOPPED = "Stopped"
|
|
||||||
DELETED = "Deleted"
|
|
||||||
|
|
||||||
|
|
||||||
class MyModel(Base):
|
class MyModel(Base):
|
||||||
@@ -21,4 +14,4 @@ class MyModel(Base):
|
|||||||
settings: Dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
creator_id: int
|
creator_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
status: Status
|
status: NodeStatus
|
||||||
|
@@ -1,16 +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.process import ProcessStatus
|
||||||
|
|
||||||
class Status(Enum):
|
|
||||||
ACTIVE = "Active"
|
|
||||||
STOPPING = "Stopping"
|
|
||||||
STOPPED = "Stopped"
|
|
||||||
DELETED = "Deleted"
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessSchema(Base):
|
class ProcessSchema(Base):
|
||||||
@@ -21,4 +14,4 @@ class ProcessSchema(Base):
|
|||||||
creator_id: int
|
creator_id: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
settings: Dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
status: Status
|
status: ProcessStatus
|
||||||
|
@@ -1,18 +1,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from api.schemas.base import Base
|
from api.schemas.base import Base
|
||||||
|
from api.db.tables.process import NodeType, NodeStatus
|
||||||
|
|
||||||
class NodeType(Enum):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
|
||||||
ACTIVE = "Active"
|
|
||||||
DISABLED = "Disabled"
|
|
||||||
DELETED = "Deleted"
|
|
||||||
|
|
||||||
|
|
||||||
class Ps_Node(Base):
|
class Ps_Node(Base):
|
||||||
@@ -22,4 +12,4 @@ class Ps_Node(Base):
|
|||||||
settings: dict
|
settings: dict
|
||||||
creator_id: Dict[str, Any]
|
creator_id: Dict[str, Any]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
status: Status
|
status: NodeStatus
|
||||||
|
@@ -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,3 +1,4 @@
|
|||||||
|
from fastapi_jwt_auth import AuthJWT
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
Request,
|
Request,
|
||||||
@@ -11,9 +12,6 @@ import re
|
|||||||
from re import escape
|
from re import escape
|
||||||
|
|
||||||
|
|
||||||
from fastapi_jwt_auth import AuthJWT
|
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
@@ -22,13 +20,18 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
|||||||
self.excluded_routes = [
|
self.excluded_routes = [
|
||||||
re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"),
|
re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"),
|
||||||
re.compile(r"^" + re.escape(self.prefix) + r"/auth/?$"),
|
re.compile(r"^" + re.escape(self.prefix) + r"/auth/?$"),
|
||||||
|
re.compile(r"^" + r"/swagger"),
|
||||||
|
re.compile(r"^" + r"/openapi"),
|
||||||
]
|
]
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
if request.method in ["GET", "POST", "PUT", "DELETE"]:
|
if request.method not in ["GET", "POST", "PUT", "DELETE"]:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
content={"detail": "Method not allowed"},
|
||||||
|
)
|
||||||
if any(pattern.match(request.url.path) for pattern in self.excluded_routes):
|
if any(pattern.match(request.url.path) for pattern in self.excluded_routes):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
else:
|
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if not auth_header:
|
if not auth_header:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -36,26 +39,15 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
|||||||
content={"detail": "Missing authorization header."},
|
content={"detail": "Missing authorization header."},
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
token = auth_header.split(" ")[1]
|
token = auth_header.split(" ")[1]
|
||||||
Authorize = AuthJWT(request)
|
Authorize = AuthJWT(request)
|
||||||
|
|
||||||
try:
|
|
||||||
current_user = Authorize.get_jwt_subject()
|
current_user = Authorize.get_jwt_subject()
|
||||||
request.state.current_user = current_user
|
request.state.current_user = current_user
|
||||||
return await call_next(request)
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
content={"detail": "The access token is invalid or expired."},
|
content={"detail": "The access token is invalid or expired."},
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
return await call_next(request)
|
||||||
# async with get_connection() as connection:
|
|
||||||
# authorize_user = await get_user_login(connection, current_user)
|
|
||||||
# print(authorize_user)
|
|
||||||
# if authorize_user is None :
|
|
||||||
# return JSONResponse(
|
|
||||||
# status_code=status.HTTP_404_NOT_FOUND ,
|
|
||||||
# detail="User not found.")
|
|
||||||
|
@@ -1,74 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
from typing import Optional
|
|
||||||
from api.schemas.endpoints.account import UserUpdate
|
|
||||||
from api.db.tables.account import KeyType, KeyStatus
|
|
||||||
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
|
|
||||||
from api.db.tables.account import AccountRole, AccountStatus
|
|
||||||
|
|
||||||
|
|
||||||
def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]:
|
|
||||||
"""
|
|
||||||
Сравнивает данные для обновления с текущими значениями пользователя.
|
|
||||||
Возвращает:
|
|
||||||
- None, если нет изменений
|
|
||||||
- Словарь {поле: новое_значение} для измененных полей
|
|
||||||
"""
|
|
||||||
update_values = {}
|
|
||||||
changes = {}
|
|
||||||
|
|
||||||
for field, value in update_data.model_dump(exclude_unset=True).items():
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(value, (AccountRole, AccountStatus)):
|
|
||||||
update_values[field] = value.value
|
|
||||||
else:
|
|
||||||
update_values[field] = value
|
|
||||||
|
|
||||||
for field, new_value in update_values.items():
|
|
||||||
if not hasattr(user, field):
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_value = getattr(user, 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
|
|
||||||
|
|
||||||
|
|
||||||
def update_key_data_changes(update_data: AccountKeyringUpdate, key) -> Optional[dict]:
|
|
||||||
"""
|
|
||||||
Сравнивает данные для обновления с текущими значениями пользователя.
|
|
||||||
Возвращает:
|
|
||||||
- None, если нет изменений
|
|
||||||
- Словарь {поле: новое_значение} для измененных полей
|
|
||||||
"""
|
|
||||||
update_values = {}
|
|
||||||
changes = {}
|
|
||||||
|
|
||||||
for field, value in update_data.model_dump(exclude_unset=True).items():
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(value, (KeyType, KeyStatus)):
|
|
||||||
update_values[field] = value.value
|
|
||||||
else:
|
|
||||||
update_values[field] = value
|
|
||||||
|
|
||||||
for field, new_value in update_values.items():
|
|
||||||
if not hasattr(key, field):
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_value = getattr(key, 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
|
|
@@ -11,3 +11,21 @@ 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_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, current_listevents_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_listevents_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_list_events_and_process_schema(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
|
||||||
|
@@ -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,10 +1,8 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.0.3"
|
version = "0.0.5"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [
|
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
|
||||||
{name = "Vladislav",email = "vlad.dev@heado.ru"}
|
|
||||||
]
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11,<4.0"
|
requires-python = ">=3.11,<4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
REACT_APP_WEBSOCKET_PROTOCOL=ws
|
VITE_APP_WEBSOCKET_PROTOCOL=ws
|
||||||
REACT_APP_HTTP_PROTOCOL=http
|
VITE_APP_HTTP_PROTOCOL=http
|
||||||
REACT_APP_API_URL=localhost:8000
|
VITE_APP_API_URL=localhost:8000
|
||||||
REACT_APP_URL=localhost:3000
|
VITE_APP_URL=localhost:3000
|
||||||
BROWSER=none
|
|
||||||
|
17
client/index.html
Normal file
17
client/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Web site created using Vite" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<title>VORKOUT</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
17607
client/package-lock.json
generated
17607
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.2",
|
"version": "0.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.6.1",
|
"@ant-design/icons": "^5.6.1",
|
||||||
@@ -9,27 +9,25 @@
|
|||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.126",
|
|
||||||
"@types/react": "^19.0.11",
|
"@types/react": "^19.0.11",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@xyflow/react": "^12.8.1",
|
||||||
"antd": "^5.24.7",
|
"antd": "^5.24.7",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"axios-retry": "^4.5.0",
|
||||||
"i18next": "^25.0.1",
|
"i18next": "^25.0.1",
|
||||||
"i18next-browser-languagedetector": "^8.0.5",
|
"i18next-browser-languagedetector": "^8.0.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-router-dom": "^7.5.0",
|
"react-router-dom": "^7.5.0",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -48,5 +46,13 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
|
"@types/node": "^20.19.1",
|
||||||
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-node-polyfills": "^0.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
10
client/public/icons/node/calculate.svg
Normal file
10
client/public/icons/node/calculate.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<mask id="mask0_381_806" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||||
|
<rect width="24" height="24" fill="#D9D9D9"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_381_806)">
|
||||||
|
<path d="M12 13.75H17V12.25H12V13.75ZM12 11.25H17V9.75H12V11.25ZM5 21C4.45 21 3.97917 20.8042 3.5875 20.4125C3.19583 20.0208 3 19.55 3 19V5C3 4.45 3.19583 3.97917 3.5875 3.5875C3.97917 3.19583 4.45 3 5 3H19C19.55 3 20.0208 3.19583 20.4125 3.5875C20.8042 3.97917 21 4.45 21 5V19C21 19.55 20.8042 20.0208 20.4125 20.4125C20.0208 20.8042 19.55 21 19 21H5ZM5 19H19V5H5V19Z" fill="#606060"/>
|
||||||
|
<path d="M9.75 14.0179H7.75V12.5179H9.75V14.0179Z" fill="#606060"/>
|
||||||
|
<path d="M9.75 11.5179H7.75V10.0179H9.75V11.5179Z" fill="#606060"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 828 B |
3
client/public/icons/node/home.svg
Normal file
3
client/public/icons/node/home.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 16H5V10H11V16H14V7L8 2.5L2 7V16ZM0 18V6L8 0L16 6V18H9V12H7V18H0Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 198 B |
8
client/public/icons/node/ifElse.svg
Normal file
8
client/public/icons/node/ifElse.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="28" height="31" viewBox="0 0 28 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 28.6L1.4 30L3 28.425L4.575 30L6 28.6L4.4 27L6 25.425L4.575 24L3 25.6L1.4 24L0 25.425L1.575 27L0 28.6Z" fill="#606060"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 0C14.1 0 15.0419 0.391472 15.8252 1.1748C16.6085 1.95814 17 2.9 17 4C17 5.1 16.6085 6.04186 15.8252 6.8252C15.0419 7.60853 14.1 8 13 8C11.9 8 10.9581 7.60853 10.1748 6.8252C9.39147 6.04186 9 5.1 9 4C9 2.9 9.39147 1.95814 10.1748 1.1748C10.9581 0.391471 11.9 0 13 0ZM13 2C12.45 2 11.9796 2.19622 11.5879 2.58789C11.1962 2.97956 11 3.45 11 4C11 4.53333 11.1962 5.00039 11.5879 5.40039C11.9795 5.80013 12.4502 6 13 6C13.5498 6 14.0205 5.80013 14.4121 5.40039C14.8038 5.00039 15 4.53333 15 4C15 3.45 14.8038 2.97956 14.4121 2.58789C14.0204 2.19622 13.55 2 13 2Z" fill="#606060"/>
|
||||||
|
<path d="M11.8 9H13.8V14H11.8V9Z" fill="#606060"/>
|
||||||
|
<path d="M20 27.55L22.825 30.375L27.775 25.425L26.35 24L22.825 27.55L21.4 26.125L20 27.55Z" fill="#606060"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.167 18.417L12.75 24.834L7.25005 19.334H4.00005L4 23H2L2.00005 17.5H7.25005L12.75 12L19.167 18.417ZM8.9229 18.417L12.75 22.2441L16.5772 18.417L12.75 14.5898L8.9229 18.417Z" fill="#606060"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 19.334L22 19.334V23H24V17.5L17.5 17.5V19.334Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>VORKOUT</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,13 +1,25 @@
|
|||||||
import React from 'react';
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import MainLayout from './pages/MainLayout';
|
import { useSetUserSelector } from './store/userStore';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
import ProtectedRoute from './pages/ProtectedRoute';
|
import ProtectedRoute from './pages/ProtectedRoute';
|
||||||
|
import MainLayout from './pages/MainLayout';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const setUser = useSetUserSelector();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
if (storedUser) {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<div>login</div>} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<ProtectedRoute />}>
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route path="*" element={<MainLayout />}></Route>
|
<Route path="*" element={<MainLayout />}></Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Access, Auth } from '../types/auth';
|
import axiosRetry from 'axios-retry';
|
||||||
|
import { Auth, Tokens } from '@/types/auth';
|
||||||
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
import { AuthService } from '@/services/authService';
|
||||||
|
import { User, UserCreate, UserUpdate } from '@/types/user';
|
||||||
|
|
||||||
const baseURL = `${process.env.REACT_APP_HTTP_PROTOCOL}://${process.env.REACT_APP_API_URL}/api/v1`;
|
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
|
||||||
|
import.meta.env.VITE_APP_API_URL
|
||||||
|
}/api/v1`;
|
||||||
|
|
||||||
const base = axios.create({
|
const base = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
@@ -11,20 +17,114 @@ const base = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// base.interceptors.request.use((config) => {
|
base.interceptors.request.use((config) => {
|
||||||
// const token = localStorage.getItem('accessToken');
|
if (config.url === '/auth/refresh') {
|
||||||
// if (token) {
|
return config;
|
||||||
// config.headers.Authorization = `Bearer ${token}`;
|
}
|
||||||
// }
|
const token = useAuthStore.getState().accessToken;
|
||||||
// return config;
|
if (token) {
|
||||||
// });
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosRetry(base, {
|
||||||
|
retries: 3,
|
||||||
|
retryDelay: (retryCount: number) => {
|
||||||
|
console.log(`retry attempt: ${retryCount}`);
|
||||||
|
return retryCount * 2000;
|
||||||
|
},
|
||||||
|
retryCondition: async (error: any) => {
|
||||||
|
if (error.code === 'ERR_CANCELED') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
base.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async function (error) {
|
||||||
|
if (!error.response) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
console.log('error', error);
|
||||||
|
const originalRequest = error.response.config;
|
||||||
|
const urlTokens = error?.request?.responseURL.split('/');
|
||||||
|
const url = urlTokens[urlTokens.length - 1];
|
||||||
|
console.log('url', url);
|
||||||
|
if (
|
||||||
|
error.response.status === 401 &&
|
||||||
|
!(originalRequest?._retry != null) &&
|
||||||
|
url !== 'login' &&
|
||||||
|
url !== 'refresh' &&
|
||||||
|
url !== 'logout'
|
||||||
|
) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
try {
|
||||||
|
await AuthService.refresh();
|
||||||
|
return base(originalRequest);
|
||||||
|
} catch (error) {
|
||||||
|
await AuthService.logout();
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
async login(auth: Auth): Promise<Access> {
|
// auth
|
||||||
console.log(auth);
|
async login(auth: Auth): Promise<Tokens> {
|
||||||
const response = await base.post<Access>('/auth', auth);
|
const response = await base.post<Tokens>('/auth', auth);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async refreshToken(): Promise<Tokens> {
|
||||||
|
const token = localStorage.getItem('refreshToken');
|
||||||
|
const response = await base.post<Tokens>(
|
||||||
|
'/auth/refresh',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// user
|
||||||
|
async getProfile(): Promise<User> {
|
||||||
|
const response = await base.get<User>('/profile');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUsers(page: number, limit: number): Promise<any> {
|
||||||
|
const response = await base.get<User[]>(
|
||||||
|
`/account?page=${page}&limit=${limit}`
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
@@ -1,161 +0,0 @@
|
|||||||
import { Drawer } from 'antd';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Avatar, Typography } from 'antd';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
interface ContentDrawerProps {
|
|
||||||
open: boolean;
|
|
||||||
closeDrawer: () => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
type: 'create' | 'edit';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ContentDrawer({
|
|
||||||
open,
|
|
||||||
closeDrawer,
|
|
||||||
children,
|
|
||||||
type,
|
|
||||||
}: ContentDrawerProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [width, setWidth] = useState<number | string>('30%');
|
|
||||||
|
|
||||||
const calculateWidths = () => {
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const expanded = Math.max(windowWidth * 0.3, 300);
|
|
||||||
setWidth(expanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
calculateWidths();
|
|
||||||
window.addEventListener('resize', calculateWidths);
|
|
||||||
return () => window.removeEventListener('resize', calculateWidths);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const editDrawerTitle = (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={closeDrawer}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '24px',
|
|
||||||
width: '24px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="./icons/drawer/arrow_back.svg"
|
|
||||||
alt="close_drawer"
|
|
||||||
style={{ height: '16px', width: '16px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
|
||||||
<Avatar
|
|
||||||
src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
|
|
||||||
size={40}
|
|
||||||
style={{ flexShrink: 0 }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Typography.Text strong style={{ display: 'block' }}>
|
|
||||||
Александр Александров
|
|
||||||
</Typography.Text>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
|
||||||
alexandralex@vorkout.ru
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '24px',
|
|
||||||
width: '24px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="./icons/drawer/delete.svg"
|
|
||||||
alt="delete"
|
|
||||||
style={{ height: '18px', width: '16px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const createDrawerTitle = (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={closeDrawer}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '24px',
|
|
||||||
width: '24px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="./icons/drawer/arrow_back.svg"
|
|
||||||
alt="close_drawer"
|
|
||||||
style={{ height: '16px', width: '16px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
flex: 1,
|
|
||||||
fontSize: '20px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('newAccount')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '24px',
|
|
||||||
width: '24px',
|
|
||||||
}}
|
|
||||||
onClick={closeDrawer}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="./icons/drawer/delete.svg"
|
|
||||||
alt="delete"
|
|
||||||
style={{ height: '18px', width: '16px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
title={type === 'create' ? createDrawerTitle : editDrawerTitle}
|
|
||||||
placement="right"
|
|
||||||
open={open}
|
|
||||||
width={width}
|
|
||||||
destroyOnClose={true}
|
|
||||||
closable={false}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -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 './drawers/ContentDrawer';
|
||||||
|
import UserEdit from './drawers/users/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="userEdit"
|
||||||
|
>
|
||||||
|
{user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
|
||||||
|
</ContentDrawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
281
client/src/components/ReactFlowDrawer.tsx
Normal file
281
client/src/components/ReactFlowDrawer.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import customEdgeStyle from "./edges/defaultEdgeStyle";
|
||||||
|
import { Dropdown, DropdownProps } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { edgeTitleGenerator } from "@/utils/edge";
|
||||||
|
import IfElseNode from "./nodes/IfElseNode";
|
||||||
|
import AppropriationNode from "./nodes/AppropriationNode";
|
||||||
|
import StartNode from "./nodes/StartNode";
|
||||||
|
|
||||||
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "startNode",
|
||||||
|
position: { x: 100, y: 0 },
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
type: "ifElse",
|
||||||
|
position: { x: 100, y: 200 },
|
||||||
|
data: { condition: "B=2" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
type: "appropriation",
|
||||||
|
position: { x: 100, y: 400 },
|
||||||
|
data: { value: "Выбрать {{account.email}}" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
type: "appropriation",
|
||||||
|
position: { x: 400, y: 400 },
|
||||||
|
data: { value: "Выбрать {{account.role}}" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = [
|
||||||
|
// {
|
||||||
|
// id: "e1-3",
|
||||||
|
// source: "1",
|
||||||
|
// sourceHandle: "1",
|
||||||
|
// target: "3",
|
||||||
|
// targetHandle: null,
|
||||||
|
// label: "A1",
|
||||||
|
// ...customEdgeStyle,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "e1-2",
|
||||||
|
// source: "1",
|
||||||
|
// sourceHandle: "2",
|
||||||
|
// target: "2",
|
||||||
|
// label: "B1",
|
||||||
|
// ...customEdgeStyle,
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ReactFlowDrawerProps {
|
||||||
|
showDrawer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReactFlowDrawer({ showDrawer }: ReactFlowDrawerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
|
const [selectedHandleId, setSelectedHandleId] = useState<string | null>(null);
|
||||||
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const handleOpenChange: DropdownProps["onOpenChange"] = (nextOpen, info) => {
|
||||||
|
if (info.source === "trigger" || nextOpen) {
|
||||||
|
setMenuVisible(nextOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent, node: Node) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const handleElement = target.closest(".react-flow__handle") as HTMLElement;
|
||||||
|
|
||||||
|
if (!handleElement) return;
|
||||||
|
|
||||||
|
const handleId = handleElement.getAttribute("data-handleid");
|
||||||
|
if (!handleId) return;
|
||||||
|
|
||||||
|
const handlePos = handleElement.getAttribute("data-handlepos");
|
||||||
|
if (handlePos === "top") return;
|
||||||
|
|
||||||
|
setSelectedHandleId(`${node.id}-${handleId}`);
|
||||||
|
|
||||||
|
const flowWrapper = document.querySelector(".react-flow") as HTMLElement;
|
||||||
|
if (flowWrapper) {
|
||||||
|
const wrapperRect = flowWrapper.getBoundingClientRect();
|
||||||
|
const handleRect = handleElement.getBoundingClientRect();
|
||||||
|
setMenuPosition({
|
||||||
|
x: handleRect.right - wrapperRect.left,
|
||||||
|
y: handleRect.top - wrapperRect.top + 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMenuVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuItemClick = (targetNodeId: string) => {
|
||||||
|
if (!selectedHandleId) return;
|
||||||
|
|
||||||
|
const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-");
|
||||||
|
|
||||||
|
const label = edgeTitleGenerator(edges.length + 1);
|
||||||
|
|
||||||
|
const newEdge: Edge = {
|
||||||
|
id: `e${sourceNodeId}-${sourceHandleId}-${targetNodeId}:${label}`,
|
||||||
|
source: sourceNodeId,
|
||||||
|
sourceHandle: sourceHandleId,
|
||||||
|
target: targetNodeId,
|
||||||
|
label,
|
||||||
|
...customEdgeStyle,
|
||||||
|
};
|
||||||
|
|
||||||
|
setEdges((eds) => [...eds, newEdge]);
|
||||||
|
setMenuVisible(false);
|
||||||
|
setSelectedHandleId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateNode = (type: string) => {
|
||||||
|
if (!selectedHandleId) return;
|
||||||
|
const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-");
|
||||||
|
const sourceNode = nodes.find((n) => n.id === sourceNodeId);
|
||||||
|
const newId = (
|
||||||
|
Math.max(...nodes.map((n) => Number(n.id) || 0)) + 1
|
||||||
|
).toString();
|
||||||
|
let newNode;
|
||||||
|
if (type === "ifElse") {
|
||||||
|
newNode = {
|
||||||
|
id: newId,
|
||||||
|
type: "ifElse",
|
||||||
|
position: {
|
||||||
|
x: (sourceNode?.position.x || 0) + 200,
|
||||||
|
y: (sourceNode?.position.y || 0) + 100,
|
||||||
|
},
|
||||||
|
data: { condition: "" },
|
||||||
|
};
|
||||||
|
} else if (type === "appropriation") {
|
||||||
|
newNode = {
|
||||||
|
id: newId,
|
||||||
|
type: "appropriation",
|
||||||
|
position: {
|
||||||
|
x: (sourceNode?.position.x || 0) + 200,
|
||||||
|
y: (sourceNode?.position.y || 0) + 100,
|
||||||
|
},
|
||||||
|
data: { value: "" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newNode) {
|
||||||
|
setNodes((nds) => [...nds, newNode]);
|
||||||
|
const label = edgeTitleGenerator(edges.length + 1);
|
||||||
|
const newEdge = {
|
||||||
|
id: `e${sourceNodeId}-${sourceHandleId}-${newId}:${label}`,
|
||||||
|
source: sourceNodeId,
|
||||||
|
sourceHandle: sourceHandleId,
|
||||||
|
target: newId,
|
||||||
|
label,
|
||||||
|
...customEdgeStyle,
|
||||||
|
};
|
||||||
|
setEdges((eds) => [...eds, newEdge]);
|
||||||
|
setMenuVisible(false);
|
||||||
|
setSelectedHandleId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newNodeTypes = [
|
||||||
|
{ key: "ifElse", label: t("ifElseNode") },
|
||||||
|
{
|
||||||
|
key: "appropriation",
|
||||||
|
label: t("appropriationNode"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const existingNodes = nodes
|
||||||
|
.filter((node) => node.id !== selectedHandleId?.split("-")[0])
|
||||||
|
.filter((node) => {
|
||||||
|
if (!selectedHandleId || node.type === "startNode") return false;
|
||||||
|
const [sourceNodeId, sourceHandleId] = selectedHandleId.split("-");
|
||||||
|
return !edges.some(
|
||||||
|
(edge) =>
|
||||||
|
edge.source === sourceNodeId &&
|
||||||
|
edge.sourceHandle === sourceHandleId &&
|
||||||
|
edge.target === node.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: "connectToExisting",
|
||||||
|
label: t("connectToExisting"),
|
||||||
|
children: existingNodes.map((node) => ({
|
||||||
|
key: node.id,
|
||||||
|
label: t("connectTo", { nodeId: node.id }),
|
||||||
|
onClick: () => handleMenuItemClick(node.id),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
...newNodeTypes.map((nt) => ({
|
||||||
|
key: `createNew-${nt.key}`,
|
||||||
|
label: nt.label,
|
||||||
|
onClick: () => handleCreateNode(nt.key),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeTypes = useMemo(
|
||||||
|
() => ({
|
||||||
|
startNode: (props: any) => <StartNode {...props} edges={edges} />,
|
||||||
|
ifElse: (props: any) => <IfElseNode {...props} edges={edges} />,
|
||||||
|
appropriation: (props: any) => (
|
||||||
|
<AppropriationNode {...props} edges={edges} />
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[edges]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
nodesDraggable={false}
|
||||||
|
elementsSelectable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
onNodeClick={(event, node) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (!target.closest(".react-flow__handle")) {
|
||||||
|
console.log("node clicked");
|
||||||
|
showDrawer();
|
||||||
|
} else {
|
||||||
|
handleClick(event, node);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
minZoom={0.5}
|
||||||
|
maxZoom={1.0}
|
||||||
|
>
|
||||||
|
<Background color="#F2F2F2" />
|
||||||
|
<Controls
|
||||||
|
position="bottom-center"
|
||||||
|
orientation="horizontal"
|
||||||
|
showInteractive={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{menuVisible && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${menuPosition.x}px`,
|
||||||
|
top: `${menuPosition.y}px`,
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: menuItems }}
|
||||||
|
open={menuVisible}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<div style={{ width: 1, height: 1 }} />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
}
|
@@ -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,7 +76,8 @@ 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>
|
||||||
@@ -82,7 +85,8 @@ export default function SiderMenu({
|
|||||||
t('accounts')
|
t('accounts')
|
||||||
),
|
),
|
||||||
style: collapseStyle,
|
style: collapseStyle,
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
{
|
{
|
||||||
key: '/events-list',
|
key: '/events-list',
|
||||||
label: !collapsed ? (
|
label: !collapsed ? (
|
||||||
|
@@ -1,229 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Upload,
|
|
||||||
Image,
|
|
||||||
UploadFile,
|
|
||||||
GetProp,
|
|
||||||
UploadProps,
|
|
||||||
} from 'antd';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
|
||||||
|
|
||||||
const getBase64 = (file: FileType): Promise<string> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result as string);
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function UserCreate() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
|
||||||
const [previewImage, setPreviewImage] = useState('');
|
|
||||||
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
|
||||||
|
|
||||||
const handlePreview = async (file: UploadFile) => {
|
|
||||||
if (!file.url && !file.preview) {
|
|
||||||
file.preview = await getBase64(file.originFileObj as FileType);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreviewImage(file.url || (file.preview as string));
|
|
||||||
setPreviewOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
|
||||||
setFileList(newFileList);
|
|
||||||
|
|
||||||
const customUploadButton = (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '102px',
|
|
||||||
width: '102px',
|
|
||||||
backgroundColor: '#E2E2E2',
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginBottom: 8,
|
|
||||||
marginTop: 30,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="./icons/drawer/add_photo_alternate.svg"
|
|
||||||
alt="add_photo_alternate"
|
|
||||||
style={{ height: '18px', width: '18px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
|
||||||
{t('selectPhoto')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const photoToUpload = (
|
|
||||||
<div style={{ height: '102px' }}>
|
|
||||||
<Upload
|
|
||||||
listType="picture-circle"
|
|
||||||
fileList={fileList}
|
|
||||||
onPreview={handlePreview}
|
|
||||||
onChange={handleChange}
|
|
||||||
beforeUpload={() => false}
|
|
||||||
>
|
|
||||||
{fileList.length > 0 ? null : customUploadButton}
|
|
||||||
</Upload>
|
|
||||||
{previewImage && (
|
|
||||||
<Image
|
|
||||||
wrapperStyle={{ display: 'none' }}
|
|
||||||
preview={{
|
|
||||||
visible: previewOpen,
|
|
||||||
onVisibleChange: (visible) => setPreviewOpen(visible),
|
|
||||||
afterOpenChange: (visible) => !visible && setPreviewImage(''),
|
|
||||||
}}
|
|
||||||
src={previewImage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginBottom: '36px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{photoToUpload}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Form
|
|
||||||
name="user-edit-form"
|
|
||||||
layout="vertical"
|
|
||||||
// onFinish={onFinish}
|
|
||||||
initialValues={{
|
|
||||||
name: '',
|
|
||||||
login: '',
|
|
||||||
password: '',
|
|
||||||
email: '',
|
|
||||||
tenant: '',
|
|
||||||
role: '',
|
|
||||||
status: '',
|
|
||||||
}}
|
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label={t('name')}
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: t('nameMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('login')}
|
|
||||||
name="login"
|
|
||||||
rules={[{ required: true, message: t('loginMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('password')}
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
|
||||||
>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('email')}
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: t('emailMessage') },
|
|
||||||
{ type: 'email', message: t('emailErrorMessage') },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('tenant')}
|
|
||||||
name="tenant"
|
|
||||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('role')}
|
|
||||||
name="role"
|
|
||||||
rules={[{ required: true, message: t('roleMessage') }]}
|
|
||||||
>
|
|
||||||
<Select placeholder={t('roleMessage')}>
|
|
||||||
<Option value="Директор магазина">Директор магазина</Option>
|
|
||||||
<Option value="Менеджер">Менеджер</Option>
|
|
||||||
<Option value="Кассир">Кассир</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('status')}
|
|
||||||
name="status"
|
|
||||||
rules={[{ required: true, message: t('statusMessage') }]}
|
|
||||||
>
|
|
||||||
<Select placeholder={t('statusMessage')}>
|
|
||||||
<Option value="ACTIVE">Активен</Option>
|
|
||||||
<Option value="DISABLED">Неактивен</Option>
|
|
||||||
<Option value="BLOCKED">Заблокирован</Option>
|
|
||||||
<Option value="DELETED">Удален</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div style={{ flexGrow: 1 }} />
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
block
|
|
||||||
style={{ color: '#000' }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/icons/drawer/reg.svg"
|
|
||||||
alt="save"
|
|
||||||
style={{ height: '18px', width: '18px' }}
|
|
||||||
/>{' '}
|
|
||||||
{t('addAccount')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,113 +0,0 @@
|
|||||||
import { Button, Form, Input, Select } from 'antd';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
export default function UserEdit() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
||||||
<Form
|
|
||||||
name="user-edit-form"
|
|
||||||
layout="vertical"
|
|
||||||
// onFinish={onFinish}
|
|
||||||
initialValues={{
|
|
||||||
name: 'Александр Александров',
|
|
||||||
login: 'alexandralex@vorkout.ru',
|
|
||||||
password: 'jKUUl776GHd',
|
|
||||||
email: 'alexandralex@vorkout.ru',
|
|
||||||
tenant: 'text',
|
|
||||||
role: 'Директор магазина',
|
|
||||||
status: 'Активен',
|
|
||||||
}}
|
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label={t('name')}
|
|
||||||
name="name"
|
|
||||||
rules={[{ required: true, message: t('nameMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('login')}
|
|
||||||
name="login"
|
|
||||||
rules={[{ required: true, message: t('loginMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('password')}
|
|
||||||
name="password"
|
|
||||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
|
||||||
>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('email')}
|
|
||||||
name="email"
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: t('emailMessage') },
|
|
||||||
{ type: 'email', message: t('emailErrorMessage') },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('tenant')}
|
|
||||||
name="tenant"
|
|
||||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('role')}
|
|
||||||
name="role"
|
|
||||||
rules={[{ required: true, message: t('roleMessage') }]}
|
|
||||||
>
|
|
||||||
<Select placeholder={t('roleMessage')}>
|
|
||||||
<Option value="Директор магазина">Директор магазина</Option>
|
|
||||||
<Option value="Менеджер">Менеджер</Option>
|
|
||||||
<Option value="Кассир">Кассир</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label={t('status')}
|
|
||||||
name="status"
|
|
||||||
rules={[{ required: true, message: t('statusMessage') }]}
|
|
||||||
>
|
|
||||||
<Select placeholder={t('statusMessage')}>
|
|
||||||
<Option value="ACTIVE">Активен</Option>
|
|
||||||
<Option value="DISABLED">Неактивен</Option>
|
|
||||||
<Option value="BLOCKED">Заблокирован</Option>
|
|
||||||
<Option value="DELETED">Удален</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<div style={{ flexGrow: 1 }} />
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
block
|
|
||||||
style={{ color: '#000' }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/icons/drawer/save.svg"
|
|
||||||
alt="save"
|
|
||||||
style={{ height: '18px', width: '18px' }}
|
|
||||||
/>{' '}
|
|
||||||
{t('save')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
245
client/src/components/drawers/ContentDrawer.tsx
Normal file
245
client/src/components/drawers/ContentDrawer.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import { Drawer } from "antd";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Avatar, Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserSelector } from "@/store/userStore";
|
||||||
|
|
||||||
|
interface ContentDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
type: "userCreate" | "userEdit" | "nodeEdit";
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContentDrawer({
|
||||||
|
open,
|
||||||
|
closeDrawer,
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
login,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
}: ContentDrawerProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [width, setWidth] = useState<number | string>("30%");
|
||||||
|
|
||||||
|
const calculateWidths = () => {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const expanded = Math.max(windowWidth * 0.3, 300);
|
||||||
|
setWidth(expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculateWidths();
|
||||||
|
window.addEventListener("resize", calculateWidths);
|
||||||
|
return () => window.removeEventListener("resize", calculateWidths);
|
||||||
|
}, []);
|
||||||
|
console.log(login, user?.login, login === user?.login);
|
||||||
|
|
||||||
|
const userEditDrawerTitle = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, flex: 1 }}>
|
||||||
|
<Avatar
|
||||||
|
src={
|
||||||
|
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||||
|
}
|
||||||
|
size={40}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ display: "block", fontSize: "20px" }}
|
||||||
|
>
|
||||||
|
{name} {login === user?.login ? t("you") : ""}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||||
|
{email}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const userCreateDrawerTitle = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("newAccount")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeEditDrawerTitle = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 12,
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Редактирование блока
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
// title={
|
||||||
|
// type === "userCreate" ? userCreateDrawerTitle : userEditDrawerTitle
|
||||||
|
// }
|
||||||
|
title={(() => {
|
||||||
|
switch (type) {
|
||||||
|
case "userCreate":
|
||||||
|
return userCreateDrawerTitle;
|
||||||
|
case "userEdit":
|
||||||
|
return userEditDrawerTitle;
|
||||||
|
case "nodeEdit":
|
||||||
|
return nodeEditDrawerTitle;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
placement="right"
|
||||||
|
open={open}
|
||||||
|
width={width}
|
||||||
|
destroyOnHidden={true}
|
||||||
|
closable={false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
213
client/src/components/drawers/DrawerTitles.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
import { User } from "@/types/user";
|
||||||
|
import { Avatar, Typography } from "antd";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
|
||||||
|
interface DrawerTitleProps {
|
||||||
|
closeDrawer: () => void;
|
||||||
|
t: TFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserEditDrawerTitleProps extends DrawerTitleProps {
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string | null;
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserCreateDrawerTitleProps extends DrawerTitleProps {}
|
||||||
|
|
||||||
|
interface NodeEditDrawerTitleProps extends DrawerTitleProps {}
|
||||||
|
|
||||||
|
const UserEditDrawerTitle = ({
|
||||||
|
closeDrawer,
|
||||||
|
login,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
user,
|
||||||
|
t,
|
||||||
|
}: UserEditDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, flex: 1 }}>
|
||||||
|
<Avatar
|
||||||
|
src={
|
||||||
|
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||||
|
}
|
||||||
|
size={40}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ display: "block", fontSize: "20px" }}
|
||||||
|
>
|
||||||
|
{name} {login === user?.login ? t("you") : ""}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||||
|
{email}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCreateDrawerTitle = ({
|
||||||
|
closeDrawer,
|
||||||
|
t,
|
||||||
|
}: UserCreateDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("newAccount")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NodeEditDrawerTitle = ({ closeDrawer, t }: NodeEditDrawerTitleProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: "16px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "12px",
|
||||||
|
flex: 1,
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("editNode")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: "18px", width: "16px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserEditDrawerTitle, UserCreateDrawerTitle, NodeEditDrawerTitle };
|
262
client/src/components/drawers/users/UserCreate.tsx
Normal file
262
client/src/components/drawers/users/UserCreate.tsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Upload,
|
||||||
|
Image,
|
||||||
|
UploadFile,
|
||||||
|
GetProp,
|
||||||
|
UploadProps,
|
||||||
|
message,
|
||||||
|
Spin,
|
||||||
|
} from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserSelector } from "@/store/userStore";
|
||||||
|
import { UserCreate as NewUserCreate } from "@/types/user";
|
||||||
|
import { UserService } from "@/services/userService";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
|
||||||
|
|
||||||
|
const getBase64 = (file: FileType): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UserCreateProps {
|
||||||
|
closeDrawer: () => void;
|
||||||
|
getUsers: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
|
const handlePreview = async (file: UploadFile) => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = await getBase64(file.originFileObj as FileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewImage(file.url || (file.preview as string));
|
||||||
|
setPreviewOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
|
||||||
|
setFileList(newFileList);
|
||||||
|
|
||||||
|
const onFinish = async (values: NewUserCreate) => {
|
||||||
|
setLoading(true);
|
||||||
|
await UserService.createUser(values);
|
||||||
|
await getUsers();
|
||||||
|
closeDrawer();
|
||||||
|
setLoading(false);
|
||||||
|
message.info(t("createdAccountMessage"), 4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const customUploadButton = (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "102px",
|
||||||
|
width: "102px",
|
||||||
|
backgroundColor: "#E2E2E2",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
marginTop: 30,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/add_photo_alternate.svg"
|
||||||
|
alt="add_photo_alternate"
|
||||||
|
style={{ height: "18px", width: "18px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={{ fontSize: "14px", color: "#8c8c8c" }}>
|
||||||
|
{t("selectPhoto")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const photoToUpload = (
|
||||||
|
<div style={{ height: "102px" }}>
|
||||||
|
<Upload
|
||||||
|
listType="picture-circle"
|
||||||
|
fileList={fileList}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
onChange={handleChange}
|
||||||
|
beforeUpload={() => false}
|
||||||
|
>
|
||||||
|
{fileList.length > 0 ? null : customUploadButton}
|
||||||
|
</Upload>
|
||||||
|
{previewImage && (
|
||||||
|
<Image
|
||||||
|
wrapperStyle={{ display: "none" }}
|
||||||
|
preview={{
|
||||||
|
visible: previewOpen,
|
||||||
|
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||||
|
afterOpenChange: (visible) => !visible && setPreviewImage(""),
|
||||||
|
}}
|
||||||
|
src={previewImage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: "36px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{photoToUpload}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
name="user-edit-form"
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={{
|
||||||
|
name: "",
|
||||||
|
login: "",
|
||||||
|
password: "",
|
||||||
|
email: "",
|
||||||
|
bindTenantId: "",
|
||||||
|
role: "",
|
||||||
|
status: "",
|
||||||
|
}}
|
||||||
|
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("name")}
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: t("nameMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("login")}
|
||||||
|
name="login"
|
||||||
|
rules={[{ required: true, message: t("loginMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("password")}
|
||||||
|
name="password"
|
||||||
|
rules={[{ message: t("passwordMessage") }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("email")}
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ message: t("emailMessage") },
|
||||||
|
{ type: "email", message: t("emailErrorMessage") },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("tenant")}
|
||||||
|
name="bindTenantId"
|
||||||
|
rules={[{ message: t("tenantMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("role")}
|
||||||
|
name="role"
|
||||||
|
rules={[{ required: true, message: t("roleMessage") }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t("roleMessage")}>
|
||||||
|
{user && user.role === "OWNER" ? (
|
||||||
|
<Option value="ADMIN">{t("ADMIN")}</Option>
|
||||||
|
) : undefined}
|
||||||
|
<Option value="EDITOR">{t("EDITOR")}</Option>
|
||||||
|
<Option value="VIEWER">{t("VIEWER")}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("status")}
|
||||||
|
name="status"
|
||||||
|
rules={[{ required: true, message: t("statusMessage") }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t("statusMessage")}>
|
||||||
|
<Option value="ACTIVE">{t("ACTIVE")}</Option>
|
||||||
|
<Option value="DISABLED">{t("DISABLED")}</Option>
|
||||||
|
<Option value="BLOCKED">{t("BLOCKED")}</Option>
|
||||||
|
<Option value="DELETED">{t("DELETED")}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
style={{ color: "#000" }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "}
|
||||||
|
{t("saving")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/reg.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: "18px", width: "18px" }}
|
||||||
|
/>{" "}
|
||||||
|
{t("addAccount")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
180
client/src/components/drawers/users/UserEdit.tsx
Normal file
180
client/src/components/drawers/users/UserEdit.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { UserService } from '@/services/userService';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
import { UserUpdate } from '@/types/user';
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Form, Input, message, Select, Spin } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface UserEditProps {
|
||||||
|
userId?: number;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
|
||||||
|
const currentUser = useUserSelector();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [user, setUser] = useState<UserUpdate>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
login: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
bindTenantId: '',
|
||||||
|
role: 'VIEWER',
|
||||||
|
meta: {},
|
||||||
|
createdAt: '',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getUser() {
|
||||||
|
if (typeof userId === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = await UserService.getUserById(userId);
|
||||||
|
setUser(user);
|
||||||
|
form.setFieldsValue({ ...user });
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFinish = async (values: UserUpdate) => {
|
||||||
|
setLoading(true);
|
||||||
|
let updatedUser: Partial<UserUpdate> = {};
|
||||||
|
|
||||||
|
(Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
|
||||||
|
if (values[key] !== user[key]) {
|
||||||
|
updatedUser = { ...updatedUser, [key]: values[key] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(updatedUser).length > 0) {
|
||||||
|
console.log('updateUser', userId, updatedUser);
|
||||||
|
await UserService.updateUser(userId!, updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
message.info(t('editAccountMessage'), 4);
|
||||||
|
closeDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="user-edit-form"
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={{ ...user }}
|
||||||
|
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t('name')}
|
||||||
|
name="name"
|
||||||
|
rules={[{ message: t('nameMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
|
<Form.Item
|
||||||
|
label={t('login')}
|
||||||
|
name="login"
|
||||||
|
rules={[{ message: t('loginMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('password')}
|
||||||
|
name="password"
|
||||||
|
rules={[{ message: t('passwordMessage') }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('email')}
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('emailMessage') },
|
||||||
|
{ type: 'email', message: t('emailErrorMessage') },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('tenant')}
|
||||||
|
name="bindTenantId"
|
||||||
|
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
|
<Form.Item
|
||||||
|
label={t('role')}
|
||||||
|
name="role"
|
||||||
|
rules={[{ required: true, message: t('roleMessage') }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t('roleMessage')}>
|
||||||
|
{currentUser && currentUser.role === 'OWNER' ? (
|
||||||
|
<Option value="ADMIN">{t('ADMIN')}</Option>
|
||||||
|
) : undefined}
|
||||||
|
<Option value="EDITOR">{t('EDITOR')}</Option>
|
||||||
|
<Option value="VIEWER">{t('VIEWER')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('status')}
|
||||||
|
name="status"
|
||||||
|
rules={[{ required: true, message: t('statusMessage') }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t('statusMessage')}>
|
||||||
|
<Option value="ACTIVE">{t('ACTIVE')}</Option>
|
||||||
|
<Option value="DISABLED">{t('DISABLED')}</Option>
|
||||||
|
<Option value="BLOCKED">{t('BLOCKED')}</Option>
|
||||||
|
<Option value="DELETED">{t('DELETED')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
style={{ color: '#000' }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '}
|
||||||
|
{t('saving')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/save.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: '18px', width: '18px' }}
|
||||||
|
/>{' '}
|
||||||
|
{t('save')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
17
client/src/components/edges/defaultEdgeStyle.tsx
Normal file
17
client/src/components/edges/defaultEdgeStyle.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { MarkerType } from "@xyflow/react";
|
||||||
|
|
||||||
|
const customEdgeStyle = {
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.Arrow,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
color: "#BDBDBD",
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
stroke: "#BDBDBD",
|
||||||
|
},
|
||||||
|
type: "step",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default customEdgeStyle;
|
63
client/src/components/nodes/AppropriationNode.tsx
Normal file
63
client/src/components/nodes/AppropriationNode.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Handle, Node, NodeProps, Position, Edge } from "@xyflow/react";
|
||||||
|
import { HANDLE_STYLE_CONNECTED } from "./defaultHandleStyle";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type AppropriationNodeData = { value: string; edges?: Edge[] };
|
||||||
|
|
||||||
|
export default function AppropriationNode({
|
||||||
|
data,
|
||||||
|
id,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<Node & AppropriationNodeData> & { edges?: Edge[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "0px solid",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "248px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "12px",
|
||||||
|
height: "48px",
|
||||||
|
fontWeight: "600px",
|
||||||
|
fontSize: "16px",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ height: "24px", width: "24px" }}
|
||||||
|
src="/icons/node/calculate.svg"
|
||||||
|
alt="appropriation logo"
|
||||||
|
/>
|
||||||
|
{t("appropriationNode")}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
minHeight: "48px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.value as string}
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
style={{
|
||||||
|
...HANDLE_STYLE_CONNECTED,
|
||||||
|
}}
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
119
client/src/components/nodes/IfElseNode.tsx
Normal file
119
client/src/components/nodes/IfElseNode.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Handle, NodeProps, Position, Node, Edge } from "@xyflow/react";
|
||||||
|
import {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_CONNECTED_V,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED_V,
|
||||||
|
} from "./defaultHandleStyle";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface IfElseNodeProps extends Node {
|
||||||
|
condition: string;
|
||||||
|
edges?: Edge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IfElseNode({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<IfElseNodeProps> & { edges?: Edge[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const isHandle1Connected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "1"
|
||||||
|
);
|
||||||
|
const isHandle2Connected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "2"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "0px solid",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "248px",
|
||||||
|
minHeight: "144px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "12px",
|
||||||
|
height: "48px",
|
||||||
|
fontWeight: "600px",
|
||||||
|
fontSize: "16px",
|
||||||
|
gap: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ height: "24px", width: "24px" }}
|
||||||
|
src="/icons/node/ifElse.svg"
|
||||||
|
alt="if else logo"
|
||||||
|
/>
|
||||||
|
{t("ifElseNode")}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
minHeight: "48px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("conditionIf", { condition: data.condition })}
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="1"
|
||||||
|
style={{
|
||||||
|
...(isHandle1Connected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED_V }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED_V }),
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
height: "48px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("conditionElse")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="input"
|
||||||
|
style={{ ...HANDLE_STYLE_CONNECTED }}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="2"
|
||||||
|
style={
|
||||||
|
isHandle2Connected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
83
client/src/components/nodes/StartNode.tsx
Normal file
83
client/src/components/nodes/StartNode.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Edge, Handle, Node, NodeProps, Position } from "@xyflow/react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
} from "./defaultHandleStyle";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface StartNodeProps extends Node {
|
||||||
|
edges?: Edge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StartNode({
|
||||||
|
id,
|
||||||
|
edges = [],
|
||||||
|
}: NodeProps<StartNodeProps> & { edges?: Edge[] }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isHandleConnected = edges.some(
|
||||||
|
(e: Edge) => e.source === id && e.sourceHandle === "1"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "0px solid",
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "248px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "12px",
|
||||||
|
height: "40px",
|
||||||
|
fontWeight: "600px",
|
||||||
|
fontSize: "16px",
|
||||||
|
gap: "12px",
|
||||||
|
backgroundColor: "#D4E0BD",
|
||||||
|
borderRadius: "8px 8px 0 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "24px",
|
||||||
|
width: "24px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ height: "16px", width: "18px" }}
|
||||||
|
src="/icons/node/home.svg"
|
||||||
|
alt="start logo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{t("startNode")}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: "1px", backgroundColor: "#E2E2E2" }}></div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("startNodeDescription")}
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="1"
|
||||||
|
style={
|
||||||
|
isHandleConnected
|
||||||
|
? { ...HANDLE_STYLE_CONNECTED }
|
||||||
|
: { ...HANDLE_STYLE_DISCONNECTED }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
34
client/src/components/nodes/defaultHandleStyle.ts
Normal file
34
client/src/components/nodes/defaultHandleStyle.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// horizontal
|
||||||
|
const HANDLE_STYLE_CONNECTED = {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
backgroundColor: "#BDBDBD",
|
||||||
|
};
|
||||||
|
|
||||||
|
// horizontal
|
||||||
|
const HANDLE_STYLE_DISCONNECTED = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
backgroundColor: "#C7D95A",
|
||||||
|
borderColor: "#606060",
|
||||||
|
borderWidth: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
const HANDLE_STYLE_CONNECTED_V = {
|
||||||
|
...HANDLE_STYLE_CONNECTED,
|
||||||
|
right: "-4px",
|
||||||
|
};
|
||||||
|
|
||||||
|
// vertical
|
||||||
|
const HANDLE_STYLE_DISCONNECTED_V = {
|
||||||
|
...HANDLE_STYLE_DISCONNECTED,
|
||||||
|
right: "-10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
HANDLE_STYLE_CONNECTED,
|
||||||
|
HANDLE_STYLE_DISCONNECTED,
|
||||||
|
HANDLE_STYLE_CONNECTED_V,
|
||||||
|
HANDLE_STYLE_DISCONNECTED_V,
|
||||||
|
};
|
@@ -1,8 +1,9 @@
|
|||||||
import './i18n';
|
import '@/config/i18n';
|
||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { theme } from './customTheme';
|
import { theme } from '@/config/customTheme';
|
||||||
|
|
||||||
import en from 'antd/locale/en_US';
|
import en from 'antd/locale/en_US';
|
||||||
import ru from 'antd/locale/ru_RU';
|
import ru from 'antd/locale/ru_RU';
|
||||||
@@ -18,7 +19,7 @@ export default function AppWrapper({ children }: any) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
|
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
|
||||||
{children}
|
<BrowserRouter>{children}</BrowserRouter>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,71 +1,119 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from "i18next";
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from "react-i18next";
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en',
|
fallbackLng: "en",
|
||||||
supportedLngs: ['en', 'ru'],
|
supportedLngs: ["en", "ru"],
|
||||||
interpolation: { escapeValue: false },
|
interpolation: { escapeValue: false },
|
||||||
resources: {
|
resources: {
|
||||||
en: {
|
en: {
|
||||||
translation: {
|
translation: {
|
||||||
accounts: 'Accounts',
|
accounts: "Accounts",
|
||||||
processDiagrams: 'Process diagrams',
|
processDiagrams: "Process diagrams",
|
||||||
runningProcesses: 'Running processes',
|
runningProcesses: "Running processes",
|
||||||
settings: 'Settings',
|
settings: "Settings",
|
||||||
eventsList: 'Events list',
|
eventsList: "Events list",
|
||||||
configuration: 'Configuration',
|
configuration: "Configuration",
|
||||||
selectPhoto: 'Select photo',
|
selectPhoto: "Select photo",
|
||||||
name: 'Name',
|
name: "Name",
|
||||||
login: 'Login',
|
login: "Login",
|
||||||
password: 'Password',
|
password: "Password",
|
||||||
email: 'Email',
|
email: "Email",
|
||||||
tenant: 'Tenant',
|
tenant: "Tenant",
|
||||||
role: 'Role',
|
role: "Role",
|
||||||
status: 'Status',
|
status: "Status",
|
||||||
nameMessage: 'Enter name',
|
nameMessage: "Enter name",
|
||||||
loginMessage: 'Enter login',
|
loginMessage: "Enter login",
|
||||||
passwordMessage: 'Enter password',
|
passwordMessage: "Enter password",
|
||||||
emailMessage: 'Enter email',
|
emailMessage: "Enter email",
|
||||||
emailErrorMessage: 'Incorrect email',
|
emailErrorMessage: "Incorrect email",
|
||||||
tenantMessage: 'Enter tenant',
|
tenantMessage: "Enter tenant",
|
||||||
roleMessage: 'Choose role',
|
roleMessage: "Choose role",
|
||||||
statusMessage: 'Choose status',
|
statusMessage: "Choose status",
|
||||||
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)",
|
||||||
|
// nodes
|
||||||
|
startNode: "Start",
|
||||||
|
startNodeDescription: "Start",
|
||||||
|
connectToExisting: "Connect to existing",
|
||||||
|
editNode: "Editing a block",
|
||||||
|
connectTo: "Connect to {{nodeId}}",
|
||||||
|
ifElseNode: "IF - ELSE",
|
||||||
|
conditionIf: "If {{condition}}, then",
|
||||||
|
conditionElse: "Else",
|
||||||
|
appropriationNode: "Appropriation",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
translation: {
|
translation: {
|
||||||
accounts: 'Учетные записи',
|
accounts: "Учетные записи",
|
||||||
processDiagrams: 'Схемы процессов',
|
processDiagrams: "Схемы процессов",
|
||||||
runningProcesses: 'Запущенные процессы',
|
runningProcesses: "Запущенные процессы",
|
||||||
settings: 'Настройки',
|
settings: "Настройки",
|
||||||
eventsList: 'Справочкин событий',
|
eventsList: "Справочкин событий",
|
||||||
configuration: 'Конфигурация',
|
configuration: "Конфигурация",
|
||||||
selectPhoto: 'Выбрать фото',
|
selectPhoto: "Выбрать фото",
|
||||||
name: 'Имя',
|
name: "Имя",
|
||||||
login: 'Логин',
|
login: "Логин",
|
||||||
password: 'Пароль',
|
password: "Пароль",
|
||||||
email: 'Имейл',
|
email: "Имейл",
|
||||||
tenant: 'Привязка',
|
tenant: "Привязка",
|
||||||
role: 'Роль',
|
role: "Роль",
|
||||||
status: 'Статус',
|
status: "Статус",
|
||||||
nameMessage: 'Введите имя',
|
nameMessage: "Введите имя",
|
||||||
loginMessage: 'Введите логин',
|
loginMessage: "Введите логин",
|
||||||
passwordMessage: 'Введите пароль',
|
passwordMessage: "Введите пароль",
|
||||||
emailMessage: 'Введите имейл',
|
emailMessage: "Введите имейл",
|
||||||
emailErrorMessage: 'Некорректный имейл',
|
emailErrorMessage: "Некорректный имейл",
|
||||||
tenantMessage: 'Введите привязку',
|
tenantMessage: "Введите привязку",
|
||||||
roleMessage: 'Выберите роль',
|
roleMessage: "Выберите роль",
|
||||||
statusMessage: 'Выберите статус',
|
statusMessage: "Выберите статус",
|
||||||
addAccount: 'Добавить аккаунт',
|
addAccount: "Добавить аккаунт",
|
||||||
save: 'Сохранить изменения',
|
save: "Сохранить изменения",
|
||||||
newAccount: 'Новая учетная запись',
|
newAccount: "Новая учетная запись",
|
||||||
|
ACTIVE: "Активен",
|
||||||
|
DISABLED: "Выключен",
|
||||||
|
BLOCKED: "Заблокирован",
|
||||||
|
DELETED: "Удален",
|
||||||
|
OWNER: "Владелец",
|
||||||
|
ADMIN: "Админ",
|
||||||
|
EDITOR: "Редактор",
|
||||||
|
VIEWER: "Наблюдатель",
|
||||||
|
nameLogin: "Имя, Логин",
|
||||||
|
createdAt: "Создано",
|
||||||
|
saving: "Сохранение...",
|
||||||
|
createdAccountMessage: "Пользователь успешно создан!",
|
||||||
|
editAccountMessage: "Пользователь успешно обновлен!",
|
||||||
|
you: "(Вы)",
|
||||||
|
// nodes
|
||||||
|
startNode: "Запуск",
|
||||||
|
startNodeDescription: "Включение",
|
||||||
|
connectToExisting: "Подключить к существующей",
|
||||||
|
editNode: "Редактирование блока",
|
||||||
|
connectTo: `Подключить к {{nodeId}}`,
|
||||||
|
ifElseNode: "ЕСЛИ - ТО",
|
||||||
|
conditionIf: "Если {{condition}}, то",
|
||||||
|
conditionElse: "Иначе",
|
||||||
|
appropriationNode: "Присвоение",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
12
client/src/env.d.ts
vendored
Normal file
12
client/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_WEBSOCKET_PROTOCOL: string;
|
||||||
|
readonly VITE_APP_HTTP_PROTOCOL: string;
|
||||||
|
readonly VITE_APP_API_URL: string;
|
||||||
|
readonly VITE_APP_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import AppWrapper from './config/AppWrapper';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<AppWrapper>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</AppWrapper>
|
|
||||||
);
|
|
16
client/src/main.tsx
Normal file
16
client/src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "@/index.css";
|
||||||
|
import App from "@/App";
|
||||||
|
import AppWrapper from "@/config/AppWrapper";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById("root") as HTMLElement
|
||||||
|
);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<AppWrapper>
|
||||||
|
<App />
|
||||||
|
</AppWrapper>
|
||||||
|
);
|
@@ -1,36 +1,219 @@
|
|||||||
import Header from '../components/Header';
|
import { useEffect, useState } from "react";
|
||||||
import { useState } from 'react';
|
import { useTranslation } from "react-i18next";
|
||||||
import ContentDrawer from '../components/ContentDrawer';
|
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
|
||||||
import UserCreate from '../components/UserCreate';
|
import Header from "@/components/Header";
|
||||||
import { useTranslation } from 'react-i18next';
|
import ContentDrawer from "@/components/drawers/ContentDrawer";
|
||||||
|
import UserCreate from "@/components/drawers/users/UserCreate";
|
||||||
|
import { Avatar, Table } from "antd";
|
||||||
|
import { TableProps } from "antd/lib";
|
||||||
|
import { UserService } from "@/services/userService";
|
||||||
|
import UserEdit from "@/components/drawers/users/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 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="userCreate"
|
||||||
|
>
|
||||||
|
<UserCreate getUsers={getUsers} closeDrawer={closeCreateDrawer} />
|
||||||
|
</ContentDrawer>
|
||||||
|
<ContentDrawer
|
||||||
|
login={activeAccount?.login}
|
||||||
|
name={activeAccount?.name}
|
||||||
|
email={activeAccount?.email}
|
||||||
|
open={openEdit}
|
||||||
|
closeDrawer={closeEditDrawer}
|
||||||
|
type="userEdit"
|
||||||
|
>
|
||||||
|
<UserEdit userId={activeAccount?.id} closeDrawer={closeEditDrawer} />
|
||||||
</ContentDrawer>
|
</ContentDrawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Header from '../components/Header';
|
|
||||||
|
|
||||||
export default function ConfigurationPage() {
|
export default function ConfigurationPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Header from '../components/Header';
|
|
||||||
|
|
||||||
export default function EventsListPage() {
|
export default function EventsListPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
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,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { AuthService } from '../services/auth';
|
|
||||||
import { Auth } from '../types/auth';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AuthService } from '@/services/authService';
|
||||||
|
import { Auth } from '@/types/auth';
|
||||||
|
|
||||||
const { Text, Link } = Typography;
|
const { Text, Link } = Typography;
|
||||||
|
|
||||||
@@ -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,22 +1,22 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Layout } from 'antd';
|
import { Layout } from "antd";
|
||||||
import Sider from 'antd/es/layout/Sider';
|
import Sider from "antd/es/layout/Sider";
|
||||||
import SiderMenu from '../components/SiderMenu';
|
import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import SiderMenu from "@/components/SiderMenu";
|
||||||
import ProcessDiagramPage from './ProcessDiagramPage';
|
import ProcessDiagramPage from "./ProcessDiagramPage";
|
||||||
import RunningProcessesPage from './RunningProcessesPage';
|
import AccountsPage from "./AccountsPage";
|
||||||
import AccountsPage from './AccountsPage';
|
import ConfigurationPage from "./ConfigurationPage";
|
||||||
import EventsListPage from './EventsListPage';
|
import EventsListPage from "./EventsListPage";
|
||||||
import ConfigurationPage from './ConfigurationPage';
|
import RunningProcessesPage from "./RunningProcessesPage";
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [selectedKey, setSelectedKey] = useState('1');
|
const [selectedKey, setSelectedKey] = useState("1");
|
||||||
|
|
||||||
const [width, setWidth] = useState<number | string>('15%');
|
const [width, setWidth] = useState<number | string>("15%");
|
||||||
const [collapsedWidth, setCollapsedWidth] = useState(50);
|
const [collapsedWidth, setCollapsedWidth] = useState(50);
|
||||||
|
|
||||||
const calculateWidths = () => {
|
const calculateWidths = () => {
|
||||||
@@ -29,24 +29,24 @@ export default function MainLayout() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateWidths();
|
calculateWidths();
|
||||||
window.addEventListener('resize', calculateWidths);
|
window.addEventListener("resize", calculateWidths);
|
||||||
return () => window.removeEventListener('resize', calculateWidths);
|
return () => window.removeEventListener("resize", calculateWidths);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname === '/') {
|
if (location.pathname === "/") {
|
||||||
navigate('/process-diagram');
|
navigate("/process-diagram");
|
||||||
}
|
}
|
||||||
setSelectedKey(location.pathname);
|
setSelectedKey(location.pathname);
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
function hangleMenuClick(e: any) {
|
function hangleMenuClick(e: any) {
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
if (key === 'toggle') {
|
if (key === "toggle") {
|
||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key === 'divider') {
|
if (key === "divider") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ export default function MainLayout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ minHeight: "100vh" }}>
|
||||||
<Sider
|
<Sider
|
||||||
className="sider"
|
className="sider"
|
||||||
collapsible
|
collapsible
|
||||||
|
@@ -1,11 +1,33 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import Header from "@/components/Header";
|
||||||
import Header from '../components/Header';
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import ReactFlowDrawer from "@/components/ReactFlowDrawer";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ContentDrawer from "@/components/drawers/ContentDrawer";
|
||||||
|
|
||||||
export default function ProcessDiagramPage() {
|
export default function ProcessDiagramPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const showDrawer = () => setIsDrawerOpen(true);
|
||||||
|
const closeDrawer = () => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title={t('processDiagrams')} />
|
<Header title={t("processDiagrams")} />
|
||||||
|
<div style={{ width: "100%", height: "100%" }}>
|
||||||
|
<ReactFlowDrawer showDrawer={showDrawer} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ContentDrawer
|
||||||
|
open={isDrawerOpen}
|
||||||
|
closeDrawer={closeDrawer}
|
||||||
|
children={undefined}
|
||||||
|
type={"nodeEdit"}
|
||||||
|
></ContentDrawer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,18 @@
|
|||||||
// ProtectedRoute.js
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
|
||||||
const ProtectedRoute = (): React.JSX.Element => {
|
const ProtectedRoute = (): React.JSX.Element => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const user = useUserSelector();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user?.id) {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
};
|
};
|
||||||
export default ProtectedRoute;
|
export default ProtectedRoute;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Header from '../components/Header';
|
|
||||||
|
|
||||||
export default function RunningProcessesPage() {
|
export default function RunningProcessesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
30
client/src/services/authService.ts
Normal file
30
client/src/services/authService.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import api from "@/api/api";
|
||||||
|
import { useAuthStore } from "@/store/authStore";
|
||||||
|
import { Auth } from "@/types/auth";
|
||||||
|
import { UserService } from "./userService";
|
||||||
|
import { useUserStore } from "@/store/userStore";
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
static async login(auth: Auth) {
|
||||||
|
const token = await api.login(auth);
|
||||||
|
useAuthStore.getState().setAccessToken(token.accessToken);
|
||||||
|
localStorage.setItem('refreshToken', token.refreshToken as string);
|
||||||
|
await UserService.getProfile().then((user) => {
|
||||||
|
useUserStore.getState().setUser(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async logout() {
|
||||||
|
console.log('logout');
|
||||||
|
useUserStore.getState().setUser(null);
|
||||||
|
useAuthStore.getState().setAccessToken(null);
|
||||||
|
localStorage.removeItem('userInfo');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async refresh() {
|
||||||
|
console.log('refresh');
|
||||||
|
const token = await api.refreshToken();
|
||||||
|
useAuthStore.getState().setAccessToken(token.accessToken);
|
||||||
|
}
|
||||||
|
}
|
38
client/src/services/userService.ts
Normal file
38
client/src/services/userService.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import api from '@/api/api';
|
||||||
|
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
static async getProfile(): Promise<User> {
|
||||||
|
console.log('getProfile');
|
||||||
|
const user = api.getProfile();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUsers(
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<AllUserResponse> {
|
||||||
|
console.log('getUsers');
|
||||||
|
const allUsers = api.getUsers(page, limit);
|
||||||
|
return allUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUserById(userId: number): Promise<User> {
|
||||||
|
console.log('getUserById');
|
||||||
|
const user = api.getUserById(userId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createUser(user: UserCreate): Promise<User> {
|
||||||
|
console.log('createUser');
|
||||||
|
const createdUser = api.createUser(user);
|
||||||
|
return createdUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||||
|
console.log('updateUser');
|
||||||
|
const updatedUser = api.updateUser(userId, user);
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
}
|
18
client/src/store/authStore.ts
Normal file
18
client/src/store/authStore.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools } from 'zustand/middleware';
|
||||||
|
|
||||||
|
type AuthState = {
|
||||||
|
accessToken: string | null;
|
||||||
|
setAccessToken: (token: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
devtools((set) => ({
|
||||||
|
accessToken: null,
|
||||||
|
setAccessToken: (token) => set({ accessToken: token }),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useAuthSelector = () => {
|
||||||
|
return useAuthStore((state) => state.accessToken);
|
||||||
|
};
|
25
client/src/store/nodeStore.ts
Normal file
25
client/src/store/nodeStore.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Edge, Node } from "@xyflow/react";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
type NodeStoreState = {
|
||||||
|
node: Node;
|
||||||
|
edge: Edge;
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeStoreActions = {
|
||||||
|
addNode: (node: Node) => void;
|
||||||
|
addEdge: (edge: Edge) => void;
|
||||||
|
removeNode: (nodeId: string) => void;
|
||||||
|
removeEdge: (edgeId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeStore = NodeStoreState & NodeStoreActions;
|
||||||
|
|
||||||
|
export const useNodeStore = create<NodeStore>()(
|
||||||
|
devtools((set) => ({
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
}))
|
||||||
|
);
|
@@ -1,16 +1,16 @@
|
|||||||
|
import { User } from '@/types/user';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { User } from '../types/user';
|
|
||||||
|
|
||||||
const userInfo = localStorage.getItem('userInfo');
|
const userInfo = localStorage.getItem('userInfo');
|
||||||
|
|
||||||
type UserStoreState = {
|
type UserStoreState = {
|
||||||
user: User;
|
user: User | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserStoreActions = {
|
type UserStoreActions = {
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserStore = UserStoreState & UserStoreActions;
|
type UserStore = UserStoreState & UserStoreActions;
|
||||||
@@ -21,7 +21,7 @@ export const useUserStore = create<UserStore>()(
|
|||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
user: userInfo != null ? JSON.parse(userInfo) : ({} as User),
|
user: userInfo != null ? JSON.parse(userInfo) : ({} as User),
|
||||||
loading: false,
|
loading: false,
|
||||||
setUser: (user: User) => set({ user }),
|
setUser: (user: User | null) => set({ user }),
|
||||||
}),
|
}),
|
||||||
{ name: 'userInfo' }
|
{ name: 'userInfo' }
|
||||||
)
|
)
|
@@ -1,4 +1,4 @@
|
|||||||
import { components } from './openapi-types';
|
import { components } from './openapi-types';
|
||||||
|
|
||||||
export type Auth = components['schemas']['Auth'];
|
export type Auth = components['schemas']['Auth'];
|
||||||
export type Access = components['schemas']['Access'];
|
export type Tokens = components['schemas']['Tokens'];
|
||||||
|
@@ -120,11 +120,6 @@ export interface paths {
|
|||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
/** Access */
|
|
||||||
Access: {
|
|
||||||
/** Accesstoken */
|
|
||||||
accessToken: string;
|
|
||||||
};
|
|
||||||
/** AccountKeyring */
|
/** AccountKeyring */
|
||||||
AccountKeyring: {
|
AccountKeyring: {
|
||||||
/** Ownerid */
|
/** Ownerid */
|
||||||
@@ -196,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: {
|
||||||
@@ -219,6 +218,13 @@ export interface components {
|
|||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
KeyType: "PASSWORD" | "ACCESS_TOKEN" | "REFRESH_TOKEN" | "API_KEY";
|
KeyType: "PASSWORD" | "ACCESS_TOKEN" | "REFRESH_TOKEN" | "API_KEY";
|
||||||
|
/** Tokens */
|
||||||
|
Tokens: {
|
||||||
|
/** Accesstoken */
|
||||||
|
accessToken: string;
|
||||||
|
/** Refreshtoken */
|
||||||
|
refreshToken?: string | null;
|
||||||
|
};
|
||||||
/** User */
|
/** User */
|
||||||
User: {
|
User: {
|
||||||
/** Id */
|
/** Id */
|
||||||
@@ -245,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 */
|
||||||
@@ -255,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;
|
||||||
@@ -305,7 +332,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["Access"];
|
"application/json": components["schemas"]["Tokens"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@@ -334,7 +361,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["Access"];
|
"application/json": components["schemas"]["Tokens"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -433,7 +460,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["UserUpdate"];
|
"application/json": components["schemas"]["UserCreate"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@@ -443,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 */
|
||||||
@@ -474,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 */
|
||||||
@@ -509,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'];
|
||||||
|
8
client/src/utils/edge.ts
Normal file
8
client/src/utils/edge.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
function edgeTitleGenerator(counter: number) {
|
||||||
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
const num = Math.ceil(counter / chars.length);
|
||||||
|
const letterIndex = (counter - 1) % chars.length;
|
||||||
|
return `${chars[letterIndex]}${num}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { edgeTitleGenerator };
|
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -18,9 +14,11 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
},
|
"baseUrl": ".",
|
||||||
"include": [
|
"paths": {
|
||||||
"src"
|
"@/*": ["./src/*"]
|
||||||
]
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
23
client/vite.config.ts
Normal file
23
client/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
port: 3000,
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@@ -32,11 +32,11 @@ startretries=5
|
|||||||
|
|
||||||
[program:client]
|
[program:client]
|
||||||
environment=
|
environment=
|
||||||
REACT_APP_WEBSOCKET_PROTOCOL=ws,
|
VITE_APP_WEBSOCKET_PROTOCOL=ws,
|
||||||
REACT_APP_HTTP_PROTOCOL=http,
|
VITE_APP_HTTP_PROTOCOL=http,
|
||||||
REACT_APP_API_URL=localhost:8000,
|
VITE_APP_API_URL=localhost:8000,
|
||||||
REACT_APP_URL=localhost:3000
|
VITE_APP_URL=localhost:3000
|
||||||
command=bash -c 'cd client; npm run build; serve -s build'
|
command=bash -c 'cd client; npm run build; npm run preview'
|
||||||
numprocs=1
|
numprocs=1
|
||||||
process_name=node-%(process_num)d
|
process_name=node-%(process_num)d
|
||||||
stdout_logfile=client.out.log
|
stdout_logfile=client.out.log
|
||||||
|
Reference in New Issue
Block a user