Compare commits
62 Commits
VORKOUT-6
...
VORKOUT-19
Author | SHA1 | Date | |
---|---|---|---|
7fd0a732b3 | |||
571186151b | |||
1b95c9825b | |||
cf6e09d8c1 | |||
|
d9e88d66f7 | ||
|
3c300f7c7a | ||
|
c66c21eb14 | ||
|
a9ecaadad6 | ||
|
e50bb32470 | ||
|
2030d54b2c | ||
|
b82960faf3 | ||
|
e0887c240f | ||
5e3c3b4672 | |||
60160d46be | |||
d03600b23d | |||
82fcd22faf | |||
|
2690843954 | ||
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 |
24
Makefile
24
Makefile
@@ -12,36 +12,35 @@ services:
|
||||
|
||||
start-api:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
python -m ${API_APPLICATION_NAME}
|
||||
poetry run python -m ${API_APPLICATION_NAME}
|
||||
|
||||
start-client:
|
||||
cd client && \
|
||||
npm start
|
||||
npm run dev
|
||||
|
||||
migrate:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args)
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic upgrade $(args)
|
||||
|
||||
downgrade:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' alembic downgrade -1
|
||||
PYTHONPATH='../..' poetry run alembic downgrade -1
|
||||
|
||||
revision:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic revision --autogenerate
|
||||
|
||||
venv-api:
|
||||
cd api && \
|
||||
poetry env activate \
|
||||
poetry install
|
||||
|
||||
venv-client:
|
||||
cd client && \
|
||||
npm install
|
||||
|
||||
install:
|
||||
make migrate head && \
|
||||
cd api && \
|
||||
@@ -58,3 +57,8 @@ format-api:
|
||||
check-api:
|
||||
cd api && \
|
||||
poetry run ruff format . --check
|
||||
|
||||
regenerate-openapi-local:
|
||||
cd client \
|
||||
rm src/types/openapi-types.ts \
|
||||
npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts
|
||||
|
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",
|
||||
)
|
||||
```
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import sys
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import loguru
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from uvicorn import run
|
||||
|
||||
from api.config import get_settings, DefaultSettings
|
||||
from api.config import DefaultSettings, get_settings
|
||||
from api.endpoints import list_of_routes
|
||||
from api.utils.common import get_hostname
|
||||
from api.services.middleware import MiddlewareAccessTokenValidadtion
|
||||
from api.utils.common import get_hostname
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
@@ -52,7 +52,6 @@ prod_origins = [""]
|
||||
|
||||
origins = dev_origins if get_settings().ENV == "local" else prod_origins
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
settings_for_application = get_settings()
|
||||
if settings_for_application.ENV == "prod":
|
||||
|
@@ -1,7 +0,0 @@
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
__all__ = [
|
||||
"metadata",
|
||||
]
|
||||
|
@@ -7,7 +7,7 @@ from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
from api.db import metadata, tables
|
||||
from orm import metadata, tables
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
|
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
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import insert, select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from api.db.tables.account import account_table
|
||||
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from orm.tables.account import account_table
|
||||
from api.schemas.account.account import User
|
||||
from api.schemas.endpoints.account import AllUserResponse, all_user_adapter
|
||||
from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate, 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 = (
|
||||
select(
|
||||
query = select(
|
||||
account_table.c.id,
|
||||
account_table.c.name,
|
||||
account_table.c.login,
|
||||
account_table.c.email,
|
||||
account_table.c.bind_tenant_id,
|
||||
account_table.c.role,
|
||||
account_table.c.meta,
|
||||
account_table.c.creator_id,
|
||||
account_table.c.created_at,
|
||||
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)
|
||||
|
||||
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)
|
||||
count_result = await connection.execute(count_query)
|
||||
|
||||
users_data = result.mappings().all()
|
||||
total_count = count_result.scalar()
|
||||
|
||||
if not total_count:
|
||||
return None
|
||||
|
||||
total_pages = math.ceil(total_count / limit)
|
||||
|
||||
validated_users = all_user_adapter.validate_python(users_data)
|
||||
|
||||
return AllUserResponse(users=validated_users, amount_count=total_count, amount_pages=total_pages)
|
||||
return AllUserResponse(
|
||||
users=validated_users,
|
||||
amount_count=total_count,
|
||||
amount_pages=total_pages,
|
||||
current_page=page,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_by_id(connection: AsyncConnection, id: int) -> Optional[User]:
|
||||
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
|
||||
"""
|
||||
Получает юзера по id.
|
||||
"""
|
||||
query = select(account_table).where(account_table.c.id == id)
|
||||
query = select(account_table).where(account_table.c.id == user_id)
|
||||
|
||||
user_db_cursor = await connection.execute(query)
|
||||
user_db = user_db_cursor.one_or_none()
|
||||
user = user_db_cursor.mappings().one_or_none()
|
||||
|
||||
if not user_db:
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user_data = {
|
||||
column.name: (
|
||||
getattr(user_db, column.name).name
|
||||
if isinstance(getattr(user_db, column.name), Enum)
|
||||
else getattr(user_db, column.name)
|
||||
)
|
||||
for column in account_table.columns
|
||||
}
|
||||
|
||||
return User.model_validate(user_data)
|
||||
return User.model_validate(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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -107,7 +156,7 @@ async def update_user_by_id(connection: AsyncConnection, update_values, user) ->
|
||||
await connection.commit()
|
||||
|
||||
|
||||
async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]:
|
||||
async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
|
||||
"""
|
||||
Создает нове поле в таблице account_table.
|
||||
"""
|
||||
@@ -117,14 +166,15 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int)
|
||||
email=user.email,
|
||||
bind_tenant_id=user.bind_tenant_id,
|
||||
role=user.role.value,
|
||||
meta=user.meta,
|
||||
meta=user.meta or {},
|
||||
creator_id=creator_id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=user.status.value,
|
||||
)
|
||||
|
||||
await connection.execute(query)
|
||||
res = await connection.execute(query)
|
||||
|
||||
await connection.commit()
|
||||
new_user = await get_user_by_id(connection, res.lastrowid)
|
||||
|
||||
return user
|
||||
return new_user
|
||||
|
@@ -4,17 +4,18 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
from enum import Enum
|
||||
|
||||
from api.db.tables.account import account_table, account_keyring_table, KeyType, KeyStatus
|
||||
from orm.tables.account import account_table, account_keyring_table, KeyType, KeyStatus
|
||||
|
||||
from api.schemas.account.account import User
|
||||
from api.schemas.account.account_keyring import AccountKeyring
|
||||
from api.schemas.endpoints.account import AllUser
|
||||
|
||||
from api.utils.key_id_gen import KeyIdGenerator
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||
async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
|
||||
query = (
|
||||
select(account_table, account_keyring_table)
|
||||
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
|
||||
@@ -45,7 +46,7 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||
for column in account_keyring_table.columns
|
||||
}
|
||||
|
||||
user = User.model_validate(user_data)
|
||||
user = AllUser.model_validate(user_data)
|
||||
password = AccountKeyring.model_validate(password_data)
|
||||
return user, password
|
||||
|
||||
|
@@ -1,13 +1,14 @@
|
||||
from typing import Optional
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import insert, select
|
||||
from sqlalchemy import insert, select, update
|
||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.tables.account import account_keyring_table
|
||||
|
||||
from orm.tables.account import account_keyring_table, KeyStatus, KeyType
|
||||
from api.schemas.account.account_keyring import AccountKeyring
|
||||
from api.utils.hasher import hasher
|
||||
|
||||
|
||||
async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
|
||||
@@ -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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -67,3 +59,37 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i
|
||||
await connection.commit()
|
||||
|
||||
return key
|
||||
|
||||
|
||||
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
|
||||
if password is None:
|
||||
password = hasher.generate_password()
|
||||
hashed_password = hasher.hash_data(password)
|
||||
stmt = mysql_insert(account_keyring_table).values(
|
||||
owner_id=owner_id,
|
||||
key_type=KeyType.PASSWORD.value,
|
||||
key_id="PASSWORD",
|
||||
key_value=hashed_password,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
expiry=datetime.now(timezone.utc) + timedelta(days=365),
|
||||
status=KeyStatus.ACTIVE,
|
||||
)
|
||||
stmt.on_duplicate_key_update(key_value=hashed_password)
|
||||
await connection.execute(stmt)
|
||||
await connection.commit()
|
||||
|
||||
|
||||
async def update_password_key(connection: AsyncConnection, owner_id: int, password: str):
|
||||
stmt = select(account_keyring_table).where(account_keyring_table.c.owner_id == owner_id)
|
||||
result = await connection.execute(stmt)
|
||||
keyring = result.one_or_none()
|
||||
if not keyring:
|
||||
await create_password_key(connection, password, owner_id)
|
||||
else:
|
||||
stmt = (
|
||||
update(account_keyring_table)
|
||||
.values(key_value=hasher.hash_data(password), expiry=datetime.now(timezone.utc) + timedelta(days=365))
|
||||
.where(account_keyring_table.c.owner_id == owner_id)
|
||||
)
|
||||
await connection.execute(stmt)
|
||||
await connection.commit()
|
||||
|
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 orm.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 orm.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
|
@@ -1,18 +0,0 @@
|
||||
__all__ = ["BigIntegerPK", "SAEnum", "UnsignedInt"]
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import BigInteger, Enum, Integer
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
|
||||
# class SAEnum(Enum):
|
||||
# def __init__(self, *enums: object, **kw: Any):
|
||||
# validate_strings = kw.pop("validate_strings", True)
|
||||
# super().__init__(*enums, **kw, validate_strings=validate_strings)
|
||||
|
||||
|
||||
# # https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer
|
||||
|
||||
# BigIntegerPK = BigInteger().with_variant(Integer, "sqlite")
|
||||
UnsignedInt = Integer().with_variant(mysql.INTEGER(unsigned=True), "mysql")
|
@@ -1 +0,0 @@
|
||||
from . import account, events, process
|
@@ -1,67 +0,0 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from api.db.sql_types import UnsignedInt
|
||||
from api.db import metadata
|
||||
|
||||
|
||||
class AccountRole(enum.StrEnum):
|
||||
OWNER = "OWNER"
|
||||
ADMIN = "ADMIN"
|
||||
EDITOR = "EDITOR"
|
||||
VIEWER = "VIEWER"
|
||||
|
||||
|
||||
class AccountStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
DISABLED = "DISABLED"
|
||||
BLOCKED = "BLOCKED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
account_table = Table(
|
||||
"account",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, primary_key=True, autoincrement=True),
|
||||
Column("name", String(100), nullable=False),
|
||||
Column("login", String(100), nullable=False),
|
||||
Column("email", String(100), nullable=True),
|
||||
Column("bind_tenant_id", String(40), nullable=True),
|
||||
Column("role", SQLAEnum(AccountRole), nullable=False),
|
||||
Column("meta", JSON, default={}),
|
||||
Column("creator_id", UnsignedInt, ForeignKey("account.id"), nullable=True),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("status", SQLAEnum(AccountStatus), nullable=False),
|
||||
Index("idx_login", "login"),
|
||||
Index("idx_name", "name"),
|
||||
)
|
||||
|
||||
|
||||
class KeyType(enum.StrEnum):
|
||||
PASSWORD = "PASSWORD"
|
||||
ACCESS_TOKEN = "ACCESS_TOKEN"
|
||||
REFRESH_TOKEN = "REFRESH_TOKEN"
|
||||
API_KEY = "API_KEY"
|
||||
|
||||
|
||||
class KeyStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
EXPIRED = "EXPIRED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
account_keyring_table = Table(
|
||||
"account_keyring",
|
||||
metadata,
|
||||
Column("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False),
|
||||
Column("key_type", SQLAEnum(KeyType), primary_key=True, nullable=False),
|
||||
Column("key_id", String(40), primary_key=True, default=None),
|
||||
Column("key_value", String(255), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("expiry", DateTime(timezone=True), nullable=True),
|
||||
Column("status", SQLAEnum(KeyStatus), nullable=False),
|
||||
)
|
@@ -1,34 +0,0 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
|
||||
from sqlalchemy.sql import func
|
||||
from enum import Enum, auto
|
||||
|
||||
from api.db.sql_types import UnsignedInt
|
||||
|
||||
from api.db import metadata
|
||||
|
||||
|
||||
class EventState(enum.StrEnum):
|
||||
AUTO = "AUTO"
|
||||
DESCRIPTED = "DESCRIPTED"
|
||||
|
||||
|
||||
class EventStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
DISABLED = "DISABLED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
list_events_table = Table(
|
||||
"list_events",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, primary_key=True, autoincrement=True),
|
||||
Column("name", String(40, collation="latin1_bin"), nullable=False, unique=True),
|
||||
Column("title", String(64), nullable=False),
|
||||
Column("creator_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("schema", JSON, default={}),
|
||||
Column("state", SQLAEnum(EventState), nullable=False),
|
||||
Column("status", SQLAEnum(EventStatus), nullable=False),
|
||||
)
|
@@ -1,108 +0,0 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import (
|
||||
Table,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
Enum as SQLAEnum,
|
||||
JSON,
|
||||
ForeignKey,
|
||||
DateTime,
|
||||
Index,
|
||||
PrimaryKeyConstraint,
|
||||
)
|
||||
from sqlalchemy.sql import func
|
||||
from enum import Enum, auto
|
||||
|
||||
from api.db.sql_types import UnsignedInt
|
||||
|
||||
from api.db import metadata
|
||||
|
||||
|
||||
class ProcessStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
STOPPING = "STOPPING"
|
||||
STOPPED = "STOPPED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
process_schema_table = Table(
|
||||
"process_schema",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, primary_key=True, autoincrement=True),
|
||||
Column("title", String(100), nullable=False),
|
||||
Column("description", Text, nullable=False),
|
||||
Column("owner_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("creator_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("settings", JSON, default={}),
|
||||
Column("status", SQLAEnum(ProcessStatus), nullable=False),
|
||||
Index(
|
||||
"idx_owner_id",
|
||||
"owner_id",
|
||||
),
|
||||
)
|
||||
|
||||
process_version_archive_table = Table(
|
||||
"process_version_archive",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, autoincrement=True, nullable=False),
|
||||
Column("ps_id", UnsignedInt, ForeignKey("process_schema.id"), nullable=False),
|
||||
Column("version", UnsignedInt, default=1, nullable=False),
|
||||
Column("snapshot", JSON, default={}),
|
||||
Column("owner_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("is_last", UnsignedInt, default=0),
|
||||
PrimaryKeyConstraint("id", "version"),
|
||||
)
|
||||
|
||||
|
||||
class NodeStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
DISABLED = "DISABLED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
class NodeType(Enum):
|
||||
TYPE1 = "Type1"
|
||||
TYPE2 = "Type2"
|
||||
TYPE3 = "Type3"
|
||||
|
||||
|
||||
ps_node_table = Table(
|
||||
"ps_node",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, autoincrement=True, primary_key=True, nullable=False),
|
||||
Column("ps_id", UnsignedInt, ForeignKey("process_schema.id"), nullable=False),
|
||||
Column("node_type", SQLAEnum(NodeType), nullable=False),
|
||||
Column("settings", JSON, default={}),
|
||||
Column("creator_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("status", SQLAEnum(NodeStatus), nullable=False),
|
||||
Index("idx_ps_id", "ps_id"),
|
||||
)
|
||||
|
||||
|
||||
class NodeLinkStatus(enum.StrEnum):
|
||||
ACTIVE = "ACTIVE"
|
||||
STOPPING = "STOPPING"
|
||||
STOPPED = "STOPPED"
|
||||
DELETED = "DELETED"
|
||||
|
||||
|
||||
node_link_table = Table(
|
||||
"node_link",
|
||||
metadata,
|
||||
Column("id", UnsignedInt, autoincrement=True, primary_key=True, nullable=False),
|
||||
Column("link_name", String(20), nullable=False),
|
||||
Column("node_id", UnsignedInt, ForeignKey("ps_node.id"), nullable=False),
|
||||
Column("next_node_id", UnsignedInt, ForeignKey("ps_node.id"), nullable=False),
|
||||
Column("settings", JSON, default={}),
|
||||
Column("creator_id", UnsignedInt, ForeignKey("account.id"), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("status", SQLAEnum(NodeLinkStatus), nullable=False),
|
||||
Index("idx_node_id", "node_id"),
|
||||
Index("idx_next_node_id", "next_node_id"),
|
||||
)
|
@@ -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.account import api_router as account_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__ = [
|
||||
"list_of_routes",
|
||||
|
@@ -1,32 +1,23 @@
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
)
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from orm.tables.account import AccountStatus
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.connection.session import get_connection_dep
|
||||
|
||||
from api.db.logic.account import (
|
||||
get_user_by_id,
|
||||
update_user_by_id,
|
||||
create_user,
|
||||
get_user_account_page_DTO,
|
||||
get_user_by_id,
|
||||
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.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.account import UserUpdate, AllUserResponse
|
||||
from api.schemas.endpoints.account import AllUserResponse, UserCreate, UserFilterDTO, UserUpdate
|
||||
from api.services.auth import get_current_user
|
||||
|
||||
from api.services.user_role_validation import db_user_role_validation
|
||||
from api.services.update_data_validation import update_user_data_changes
|
||||
|
||||
|
||||
api_router = APIRouter(
|
||||
prefix="/account",
|
||||
@@ -36,14 +27,33 @@ api_router = APIRouter(
|
||||
|
||||
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllUserResponse)
|
||||
async def get_all_account(
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
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 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),
|
||||
current_user=Depends(get_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:
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -67,24 +79,25 @@ async def get_account(
|
||||
|
||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
async def create_account(
|
||||
user: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
|
||||
user: UserCreate,
|
||||
connection: AsyncConnection = Depends(get_connection_dep),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
authorize_user = await db_user_role_validation(connection, current_user)
|
||||
|
||||
user_validation = await get_user_by_login(connection, user.login)
|
||||
|
||||
if user_validation is None:
|
||||
await create_user(connection, user, authorize_user.id)
|
||||
user_new = await get_user_by_login(connection, user.login)
|
||||
return user_new
|
||||
|
||||
new_user = await create_user(connection, user, authorize_user.id)
|
||||
await create_password_key(connection, user.password, new_user.id)
|
||||
return new_user
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
|
||||
)
|
||||
|
||||
|
||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
|
||||
async def update_account(
|
||||
user_id: int,
|
||||
user_update: UserUpdate,
|
||||
@@ -97,14 +110,15 @@ async def update_account(
|
||||
if user is None:
|
||||
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
|
||||
|
||||
user_update_data = User.model_validate({**user.model_dump(), **update_values})
|
||||
|
||||
await update_user_by_id(connection, update_values, user)
|
||||
await update_user_by_id(connection, updated_values, user)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -123,12 +139,12 @@ async def delete_account(
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
@@ -1,31 +1,19 @@
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Body,
|
||||
Depends,
|
||||
Form,
|
||||
HTTPException,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
|
||||
|
||||
from orm.tables.account import KeyStatus
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.connection.session import get_connection_dep
|
||||
|
||||
from api.db.logic.keyring import get_key_by_id, create_key, update_key_by_id
|
||||
|
||||
|
||||
from api.db.tables.account import KeyStatus
|
||||
from api.db.logic.keyring import create_key, get_key_by_id, update_key_by_id
|
||||
from api.schemas.account.account_keyring import AccountKeyring
|
||||
from api.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
|
||||
|
||||
from api.schemas.account.account_keyring import AccountKeyring
|
||||
from api.services.auth import get_current_user
|
||||
|
||||
from api.services.user_role_validation import db_user_role_validation
|
||||
from api.services.update_data_validation import update_key_data_changes
|
||||
|
||||
|
||||
api_router = APIRouter(
|
||||
prefix="/keyring",
|
||||
@@ -87,14 +75,12 @@ async def update_keyring(
|
||||
if keyring is None:
|
||||
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
|
||||
|
||||
keyring_update_data = AccountKeyring.model_validate({**keyring.model_dump(), **update_values})
|
||||
|
||||
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)
|
||||
|
||||
@@ -116,12 +102,12 @@ async def delete_keyring(
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
169
api/api/endpoints/list_events.py
Normal file
169
api/api/endpoints/list_events.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from orm.tables.events import EventStatus
|
||||
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 (
|
||||
create_list_events,
|
||||
get_list_events_by_id,
|
||||
get_list_events_by_name,
|
||||
get_list_events_page_DTO,
|
||||
update_list_events_by_id,
|
||||
)
|
||||
from api.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.list_events import AllListEventResponse, ListEventFilterDTO, ListEventUpdate
|
||||
from api.schemas.events.list_events import ListEvent
|
||||
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,
|
||||
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||
)
|
||||
|
||||
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
|
169
api/api/endpoints/process_schema.py
Normal file
169
api/api/endpoints/process_schema.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from orm.tables.process import ProcessStatus
|
||||
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 (
|
||||
create_process_schema,
|
||||
get_process_schema_by_id,
|
||||
get_process_schema_by_title,
|
||||
get_process_schema_page_DTO,
|
||||
update_process_schema_by_id,
|
||||
)
|
||||
from api.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.process_schema import AllProcessSchemaResponse, ProcessSchemaFilterDTO, ProcessSchemaUpdate
|
||||
from api.schemas.process.process_schema import ProcessSchema
|
||||
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,
|
||||
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||
)
|
||||
|
||||
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,26 +1,17 @@
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Body,
|
||||
Depends,
|
||||
Form,
|
||||
HTTPException,
|
||||
Request,
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.connection.session import get_connection_dep
|
||||
from api.db.logic.account import get_user_by_id, update_user_by_id, get_user_by_login
|
||||
from api.schemas.base import bearer_schema
|
||||
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.db.logic.account import get_user_by_id, get_user_by_login, update_user_by_id
|
||||
from api.schemas.account.account import User
|
||||
|
||||
from api.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.account import UserUpdate
|
||||
from api.services.auth import get_current_user
|
||||
|
||||
api_router = APIRouter(
|
||||
prefix="/profile",
|
||||
@@ -42,7 +33,7 @@ async def get_profile(
|
||||
|
||||
@api_router.put("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
async def update_profile(
|
||||
user_updata: UserUpdate,
|
||||
user_update: UserUpdate,
|
||||
connection: AsyncConnection = Depends(get_connection_dep),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
@@ -50,13 +41,13 @@ async def update_profile(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||
|
||||
if user_updata.role == None and user_updata.login == None:
|
||||
update_values = update_user_data_changes(user_updata, user)
|
||||
if user_update.role is None and user_update.login is None:
|
||||
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||
|
||||
if update_values is None:
|
||||
if updated_values is None:
|
||||
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)
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import datetime
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from orm.tables.account import AccountRole, AccountStatus
|
||||
from pydantic import EmailStr, Field
|
||||
from api.db.tables.account import AccountRole, AccountStatus
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import datetime
|
||||
from typing import Optional
|
||||
from pydantic import Field
|
||||
from datetime import datetime
|
||||
from api.db.tables.account import KeyType, KeyStatus
|
||||
from typing import Optional
|
||||
|
||||
from orm.tables.account import KeyStatus, KeyType
|
||||
from pydantic import Field
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
@@ -1,25 +1,34 @@
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from pydantic import EmailStr, Field, TypeAdapter
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from api.db.tables.account import AccountRole, AccountStatus
|
||||
from orm.tables.account import AccountRole, AccountStatus
|
||||
from pydantic import EmailStr, Field, TypeAdapter
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
class UserUpdate(Base):
|
||||
id: Optional[int] = None
|
||||
name: Optional[str] = Field(None, max_length=100)
|
||||
login: Optional[str] = Field(None, max_length=100)
|
||||
email: Optional[EmailStr] = None
|
||||
password: Optional[str] = None
|
||||
bind_tenant_id: Optional[str] = Field(None, max_length=40)
|
||||
role: Optional[AccountRole] = None
|
||||
meta: Optional[dict] = None
|
||||
creator_id: Optional[int] = None
|
||||
created_at: Optional[datetime] = 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):
|
||||
id: int
|
||||
name: str
|
||||
@@ -27,6 +36,8 @@ class AllUser(Base):
|
||||
email: Optional[EmailStr] = None
|
||||
bind_tenant_id: Optional[str] = None
|
||||
role: AccountRole
|
||||
meta: Optional[dict] = None
|
||||
creator_id: Optional[int] = None
|
||||
created_at: datetime
|
||||
status: AccountStatus
|
||||
|
||||
@@ -35,6 +46,15 @@ class AllUserResponse(Base):
|
||||
users: List[AllUser]
|
||||
amount_count: int
|
||||
amount_pages: int
|
||||
current_page: int
|
||||
limit: int
|
||||
|
||||
|
||||
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,8 +1,7 @@
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from orm.tables.account import KeyStatus, KeyType
|
||||
from pydantic import Field
|
||||
from datetime import datetime
|
||||
from api.db.tables.account import KeyType, KeyStatus
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
@@ -10,8 +9,5 @@ from api.schemas.base import Base
|
||||
class AccountKeyringUpdate(Base):
|
||||
owner_id: Optional[int] = None
|
||||
key_type: Optional[KeyType] = None
|
||||
key_id: Optional[str] = Field(None, max_length=40)
|
||||
key_value: Optional[str] = Field(None, max_length=255)
|
||||
created_at: Optional[datetime] = None
|
||||
expiry: Optional[datetime] = None
|
||||
status: Optional[KeyStatus] = None
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
# Таблица для получения информации из запроса
|
||||
|
||||
|
||||
|
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 datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from orm.tables.events import EventState, EventStatus
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
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 datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from orm.tables.process import ProcessStatus
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
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,28 +1,18 @@
|
||||
from pydantic import Field
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
from orm.tables.events import EventState, EventStatus
|
||||
from pydantic import Field
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
class State(Enum):
|
||||
AUTO = "Auto"
|
||||
DESCRIPTED = "Descripted"
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "Active"
|
||||
DISABLED = "Disabled"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class ListEvent(Base):
|
||||
id: int
|
||||
name: str = Field(..., max_length=40)
|
||||
title: str = Field(..., max_length=64)
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
schema: Dict[str, Any]
|
||||
state: State
|
||||
status: Status
|
||||
schema_: Dict[str, Any] = Field(..., alias="schema")
|
||||
state: EventState
|
||||
status: EventStatus
|
||||
|
@@ -1,18 +1,12 @@
|
||||
from pydantic import Field
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
from orm.tables.process import NodeStatus
|
||||
from pydantic import Field
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "Active"
|
||||
STOPPING = "Stopping"
|
||||
STOPPED = "Stopped"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class MyModel(Base):
|
||||
id: int
|
||||
link_name: str = Field(..., max_length=20)
|
||||
@@ -21,4 +15,4 @@ class MyModel(Base):
|
||||
settings: Dict[str, Any]
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
status: Status
|
||||
status: NodeStatus
|
||||
|
@@ -1,18 +1,12 @@
|
||||
from pydantic import Field
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
from orm.tables.process import ProcessStatus
|
||||
from pydantic import Field
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "Active"
|
||||
STOPPING = "Stopping"
|
||||
STOPPED = "Stopped"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class ProcessSchema(Base):
|
||||
id: int
|
||||
title: str = Field(..., max_length=100)
|
||||
@@ -21,4 +15,4 @@ class ProcessSchema(Base):
|
||||
creator_id: int
|
||||
created_at: datetime
|
||||
settings: Dict[str, Any]
|
||||
status: Status
|
||||
status: ProcessStatus
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
@@ -1,20 +1,11 @@
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
from enum import Enum
|
||||
from typing import Any, Dict
|
||||
|
||||
from orm.tables.process import NodeStatus, NodeType
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
class NodeType(Enum):
|
||||
pass
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
ACTIVE = "Active"
|
||||
DISABLED = "Disabled"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class Ps_Node(Base):
|
||||
id: int
|
||||
ps_id: int
|
||||
@@ -22,4 +13,4 @@ class Ps_Node(Base):
|
||||
settings: dict
|
||||
creator_id: Dict[str, Any]
|
||||
created_at: datetime
|
||||
status: Status
|
||||
status: NodeStatus
|
||||
|
@@ -1,27 +1,25 @@
|
||||
from fastapi import Request, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from orm.tables.account import AccountStatus
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.logic.auth import get_user
|
||||
|
||||
# # from backend.schemas.users.token import TokenData
|
||||
from api.schemas.account.account import User
|
||||
from api.db.tables.account import AccountStatus
|
||||
|
||||
from api.utils.hasher import Hasher
|
||||
from api.schemas.endpoints.account import AllUser
|
||||
from api.utils.hasher import hasher
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> Optional[User]:
|
||||
async def get_current_user(request: Request) -> str | HTTPException:
|
||||
if not hasattr(request.state, "current_user"):
|
||||
return HTTPException(status_code=401, detail="Unauthorized")
|
||||
return request.state.current_user
|
||||
|
||||
|
||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[User]:
|
||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[AllUser]:
|
||||
sql_user, sql_password = await get_user(connection, username)
|
||||
|
||||
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
||||
return None
|
||||
hasher = Hasher()
|
||||
if not hasher.verify_data(password, sql_password.key_value):
|
||||
return None
|
||||
return sql_user
|
||||
|
@@ -1,15 +1,15 @@
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
import re
|
||||
from re import escape
|
||||
|
||||
from fastapi import (
|
||||
Request,
|
||||
status,
|
||||
)
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
from api.config import get_settings
|
||||
from fastapi_jwt_auth import AuthJWT
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
import re
|
||||
from re import escape
|
||||
from api.config import get_settings
|
||||
|
||||
|
||||
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
||||
|
@@ -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
|
@@ -2,8 +2,9 @@ from fastapi import (
|
||||
HTTPException,
|
||||
status,
|
||||
)
|
||||
from orm.tables.account import AccountRole
|
||||
|
||||
from api.db.logic.account import get_user_by_login
|
||||
from api.db.tables.account import AccountRole
|
||||
|
||||
|
||||
async def db_user_role_validation(connection, current_user):
|
||||
@@ -11,3 +12,21 @@ async def db_user_role_validation(connection, current_user):
|
||||
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")
|
||||
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 secrets
|
||||
|
||||
|
||||
# Хешер для работы с паролем.
|
||||
|
||||
@@ -14,3 +16,10 @@ class Hasher:
|
||||
def verify_data(self, password: str, hashed: str) -> bool:
|
||||
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
||||
return self.hash_data(password) == hashed
|
||||
|
||||
@staticmethod
|
||||
def generate_password() -> str:
|
||||
return secrets.token_urlsafe(20)
|
||||
|
||||
|
||||
hasher = Hasher()
|
||||
|
@@ -1,32 +1,24 @@
|
||||
import os
|
||||
import asyncio
|
||||
import hashlib
|
||||
import secrets
|
||||
import os
|
||||
|
||||
from orm.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
|
||||
|
||||
from api.db.connection.session import get_connection
|
||||
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
|
||||
from api.utils.hasher import hasher
|
||||
from api.utils.key_id_gen import KeyIdGenerator
|
||||
|
||||
INIT_LOCK_FILE = "../init.lock"
|
||||
DEFAULT_LOGIN = "vorkout"
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
|
||||
def generate_password() -> str:
|
||||
return secrets.token_urlsafe(20)
|
||||
|
||||
|
||||
async def init():
|
||||
if os.path.exists(INIT_LOCK_FILE):
|
||||
print("Sorry, service is already initialized")
|
||||
return
|
||||
|
||||
async with get_connection() as conn:
|
||||
password = generate_password()
|
||||
hashed_password = hash_password(password)
|
||||
password = hasher.generate_password()
|
||||
hashed_password = hasher.hash_data(password)
|
||||
|
||||
create_user_query = account_table.insert().values(
|
||||
name=DEFAULT_LOGIN,
|
||||
|
153
api/poetry.lock
generated
153
api/poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aio-pika"
|
||||
@@ -227,6 +227,25 @@ files = [
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-library"
|
||||
version = "0.1.0"
|
||||
description = "Abstract classes library for the Vorkout project"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = []
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
sqlalchemy = ">=2.0.43,<3.0.0"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://gitea.heado.ru/Vorkout/core.git"
|
||||
reference = "0.1.0"
|
||||
resolved_reference = "96ddb52582660600dbacead4919e67f948e96898"
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.2"
|
||||
@@ -1416,83 +1435,83 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.39"
|
||||
version = "2.0.43"
|
||||
description = "Database Abstraction Library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:66a40003bc244e4ad86b72abb9965d304726d05a939e8c09ce844d27af9e6d37"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67de057fbcb04a066171bd9ee6bcb58738d89378ee3cabff0bffbf343ae1c787"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:533e0f66c32093a987a30df3ad6ed21170db9d581d0b38e71396c49718fbb1ca"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7399d45b62d755e9ebba94eb89437f80512c08edde8c63716552a3aade61eb42"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:788b6ff6728072b313802be13e88113c33696a9a1f2f6d634a97c20f7ef5ccce"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-win32.whl", hash = "sha256:01da15490c9df352fbc29859d3c7ba9cd1377791faeeb47c100832004c99472c"},
|
||||
{file = "SQLAlchemy-2.0.39-cp37-cp37m-win_amd64.whl", hash = "sha256:f2bcb085faffcacf9319b1b1445a7e1cfdc6fb46c03f2dce7bc2d9a4b3c1cdc5"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b761a6847f96fdc2d002e29e9e9ac2439c13b919adfd64e8ef49e75f6355c548"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d7e3866eb52d914aea50c9be74184a0feb86f9af8aaaa4daefe52b69378db0b"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995c2bacdddcb640c2ca558e6760383dcdd68830160af92b5c6e6928ffd259b4"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344cd1ec2b3c6bdd5dfde7ba7e3b879e0f8dd44181f16b895940be9b842fd2b6"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5dfbc543578058c340360f851ddcecd7a1e26b0d9b5b69259b526da9edfa8875"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3395e7ed89c6d264d38bea3bfb22ffe868f906a7985d03546ec7dc30221ea980"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-win32.whl", hash = "sha256:bf555f3e25ac3a70c67807b2949bfe15f377a40df84b71ab2c58d8593a1e036e"},
|
||||
{file = "SQLAlchemy-2.0.39-cp38-cp38-win_amd64.whl", hash = "sha256:463ecfb907b256e94bfe7bcb31a6d8c7bc96eca7cbe39803e448a58bb9fcad02"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6827f8c1b2f13f1420545bd6d5b3f9e0b85fe750388425be53d23c760dcf176b"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9f119e7736967c0ea03aff91ac7d04555ee038caf89bb855d93bbd04ae85b41"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4600c7a659d381146e1160235918826c50c80994e07c5b26946a3e7ec6c99249"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a06e6c8e31c98ddc770734c63903e39f1947c9e3e5e4bef515c5491b7737dde"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4c433f78c2908ae352848f56589c02b982d0e741b7905228fad628999799de4"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bd5c5ee1448b6408734eaa29c0d820d061ae18cb17232ce37848376dcfa3e92"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-win32.whl", hash = "sha256:87a1ce1f5e5dc4b6f4e0aac34e7bb535cb23bd4f5d9c799ed1633b65c2bcad8c"},
|
||||
{file = "sqlalchemy-2.0.39-cp310-cp310-win_amd64.whl", hash = "sha256:871f55e478b5a648c08dd24af44345406d0e636ffe021d64c9b57a4a11518304"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a28f9c238f1e143ff42ab3ba27990dfb964e5d413c0eb001b88794c5c4a528a9"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08cf721bbd4391a0e765fe0fe8816e81d9f43cece54fdb5ac465c56efafecb3d"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a8517b6d4005facdbd7eb4e8cf54797dbca100a7df459fdaff4c5123265c1cd"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b2de1523d46e7016afc7e42db239bd41f2163316935de7c84d0e19af7e69538"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:412c6c126369ddae171c13987b38df5122cb92015cba6f9ee1193b867f3f1530"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b35e07f1d57b79b86a7de8ecdcefb78485dab9851b9638c2c793c50203b2ae8"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-win32.whl", hash = "sha256:3eb14ba1a9d07c88669b7faf8f589be67871d6409305e73e036321d89f1d904e"},
|
||||
{file = "sqlalchemy-2.0.39-cp311-cp311-win_amd64.whl", hash = "sha256:78f1b79132a69fe8bd6b5d91ef433c8eb40688ba782b26f8c9f3d2d9ca23626f"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109"},
|
||||
{file = "sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7"},
|
||||
{file = "sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2600a50d590c22d99c424c394236899ba72f849a02b10e65b4c70149606408b5"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4eff9c270afd23e2746e921e80182872058a7a592017b2713f33f96cc5f82e32"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7332868ce891eda48896131991f7f2be572d65b41a4050957242f8e935d5d7"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125a7763b263218a80759ad9ae2f3610aaf2c2fbbd78fff088d584edf81f3782"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:04545042969833cb92e13b0a3019549d284fd2423f318b6ba10e7aa687690a3c"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:805cb481474e111ee3687c9047c5f3286e62496f09c0e82e8853338aaaa348f8"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-win32.whl", hash = "sha256:34d5c49f18778a3665d707e6286545a30339ad545950773d43977e504815fa70"},
|
||||
{file = "sqlalchemy-2.0.39-cp39-cp39-win_amd64.whl", hash = "sha256:35e72518615aa5384ef4fae828e3af1b43102458b74a8c481f69af8abf7e802a"},
|
||||
{file = "sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f"},
|
||||
{file = "sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07097c0a1886c150ef2adba2ff7437e84d40c0f7dcb44a2c2b9c905ccfc6361c"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cdeff998cb294896a34e5b2f00e383e7c5c4ef3b4bfa375d9104723f15186443"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:bcf0724a62a5670e5718957e05c56ec2d6850267ea859f8ad2481838f889b42c"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-win32.whl", hash = "sha256:c697575d0e2b0a5f0433f679bda22f63873821d991e95a90e9e52aae517b2e32"},
|
||||
{file = "SQLAlchemy-2.0.43-cp37-cp37m-win_amd64.whl", hash = "sha256:d34c0f6dbefd2e816e8f341d0df7d4763d382e3f452423e752ffd1e213da2512"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70322986c0c699dca241418fcf18e637a4369e0ec50540a2b907b184c8bca069"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87accdbba88f33efa7b592dc2e8b2a9c2cdbca73db2f9d5c510790428c09c154"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00e7845d2f692ebfc7d5e4ec1a3fd87698e4337d09e58d6749a16aedfdf8612"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:022e436a1cb39b13756cf93b48ecce7aa95382b9cfacceb80a7d263129dfd019"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5e73ba0d76eefc82ec0219d2301cb33bfe5205ed7a2602523111e2e56ccbd20"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c2e02f06c68092b875d5cbe4824238ab93a7fa35d9c38052c033f7ca45daa18"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-win32.whl", hash = "sha256:e7a903b5b45b0d9fa03ac6a331e1c1d6b7e0ab41c63b6217b3d10357b83c8b00"},
|
||||
{file = "sqlalchemy-2.0.43-cp310-cp310-win_amd64.whl", hash = "sha256:4bf0edb24c128b7be0c61cd17eef432e4bef507013292415f3fb7023f02b7d4b"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921"},
|
||||
{file = "sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d"},
|
||||
{file = "sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d"},
|
||||
{file = "sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4e6aeb2e0932f32950cf56a8b4813cb15ff792fc0c9b3752eaf067cfe298496a"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f964a05356f4bca4112e6334ed7c208174511bd56e6b8fc86dad4d024d4185"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46293c39252f93ea0910aababa8752ad628bcce3a10d3f260648dd472256983f"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136063a68644eca9339d02e6693932116f6a8591ac013b0014479a1de664e40a"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6e2bf13d9256398d037fef09fd8bf9b0bf77876e22647d10761d35593b9ac547"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:44337823462291f17f994d64282a71c51d738fc9ef561bf265f1d0fd9116a782"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-win32.whl", hash = "sha256:13194276e69bb2af56198fef7909d48fd34820de01d9c92711a5fa45497cc7ed"},
|
||||
{file = "sqlalchemy-2.0.43-cp38-cp38-win_amd64.whl", hash = "sha256:334f41fa28de9f9be4b78445e68530da3c5fa054c907176460c81494f4ae1f5e"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb5c832cc30663aeaf5e39657712f4c4241ad1f638d487ef7216258f6d41fe7"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11f43c39b4b2ec755573952bbcc58d976779d482f6f832d7f33a8d869ae891bf"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413391b2239db55be14fa4223034d7e13325a1812c8396ecd4f2c08696d5ccad"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c379e37b08c6c527181a397212346be39319fb64323741d23e46abd97a400d34"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03d73ab2a37d9e40dec4984d1813d7878e01dbdc742448d44a7341b7a9f408c7"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8cee08f15d9e238ede42e9bbc1d6e7158d0ca4f176e4eab21f88ac819ae3bd7b"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-win32.whl", hash = "sha256:b3edaec7e8b6dc5cd94523c6df4f294014df67097c8217a89929c99975811414"},
|
||||
{file = "sqlalchemy-2.0.43-cp39-cp39-win_amd64.whl", hash = "sha256:227119ce0a89e762ecd882dc661e0aa677a690c914e358f0dd8932a2e8b2765b"},
|
||||
{file = "sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc"},
|
||||
{file = "sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiomysql = {version = ">=0.2.0", optional = true, markers = "extra == \"aiomysql\""}
|
||||
greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"aiomysql\""}
|
||||
greenlet = {version = ">=1", optional = true, markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"aiomysql\""}
|
||||
pymysql = {version = "*", optional = true, markers = "extra == \"pymysql\""}
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[package.extras]
|
||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
|
||||
aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (!=0.4.17)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
|
||||
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
|
||||
aioodbc = ["aioodbc", "greenlet (>=1)"]
|
||||
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
|
||||
asyncio = ["greenlet (>=1)"]
|
||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
|
||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
|
||||
mssql = ["pyodbc"]
|
||||
mssql-pymssql = ["pymssql"]
|
||||
@@ -1503,7 +1522,7 @@ mysql-connector = ["mysql-connector-python"]
|
||||
oracle = ["cx_oracle (>=8)"]
|
||||
oracle-oracledb = ["oracledb (>=1.0.1)"]
|
||||
postgresql = ["psycopg2 (>=2.7)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||
postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"]
|
||||
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
|
||||
postgresql-psycopg = ["psycopg (>=3.0.7)"]
|
||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
||||
@@ -1933,4 +1952,4 @@ propcache = ">=0.2.0"
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "5ed129fde2c5d7b3518fbb2fe2ce79f0bad2aa1060304d45a7bc26d35f7ab46b"
|
||||
content-hash = "0ac41110571b3bba38672fceef1c107864b8582378c6afba6418e05f99a75da1"
|
||||
|
@@ -1,10 +1,8 @@
|
||||
[project]
|
||||
name = "api"
|
||||
version = "0.0.3"
|
||||
version = "0.0.5"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Vladislav",email = "vlad.dev@heado.ru"}
|
||||
]
|
||||
authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11,<4.0"
|
||||
dependencies = [
|
||||
@@ -19,6 +17,7 @@ dependencies = [
|
||||
"pydantic[email] (>=2.11.3,<3.0.0)",
|
||||
"python-multipart (>=0.0.20,<0.0.21)",
|
||||
"fastapi-jwt-auth @ git+https://github.com/vvpreo/fastapi-jwt-auth",
|
||||
"core-library @ git+https://gitea.heado.ru/Vorkout/core.git@0.1.0",
|
||||
]
|
||||
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
REACT_APP_WEBSOCKET_PROTOCOL=ws
|
||||
REACT_APP_HTTP_PROTOCOL=http
|
||||
REACT_APP_API_URL=localhost:8000
|
||||
REACT_APP_URL=localhost:3000
|
||||
BROWSER=none
|
||||
VITE_APP_WEBSOCKET_PROTOCOL=ws
|
||||
VITE_APP_HTTP_PROTOCOL=http
|
||||
VITE_APP_API_URL=localhost:8000
|
||||
VITE_APP_URL=localhost:3000
|
||||
|
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>
|
17454
client/package-lock.json
generated
17454
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
@@ -9,7 +9,6 @@
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.126",
|
||||
"@types/react": "^19.0.11",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"antd": "^5.24.7",
|
||||
@@ -21,16 +20,13 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4",
|
||||
"zustand": "^5.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -49,5 +45,13 @@
|
||||
"last 1 firefox 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 |
@@ -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,10 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import MainLayout from './pages/MainLayout';
|
||||
import ProtectedRoute from './pages/ProtectedRoute';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import { useSetUserSelector } from './store/userStore';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import ProtectedRoute from './pages/ProtectedRoute';
|
||||
import MainLayout from './pages/MainLayout';
|
||||
|
||||
function App() {
|
||||
const setUser = useSetUserSelector();
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import axios from 'axios';
|
||||
import { Auth, Tokens } from '../types/auth';
|
||||
import { User } from '../types/user';
|
||||
import { AuthService } from '../services/authService';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
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({
|
||||
baseURL,
|
||||
@@ -45,6 +47,9 @@ base.interceptors.response.use(
|
||||
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('/');
|
||||
@@ -103,6 +108,23 @@ const api = {
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getUserById(userId: number): Promise<User> {
|
||||
const response = await base.get<User>(`/account/${userId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createUser(user: UserCreate): Promise<User> {
|
||||
const response = await base.post<User>('/account', user);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||
const response = await base.put<User>(`/account/${userId}`, user);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// keyrings
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
@@ -2,12 +2,16 @@ import { Drawer } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Avatar, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
|
||||
interface ContentDrawerProps {
|
||||
open: boolean;
|
||||
closeDrawer: () => void;
|
||||
children: React.ReactNode;
|
||||
type: 'create' | 'edit';
|
||||
login?: string;
|
||||
name?: string;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export default function ContentDrawer({
|
||||
@@ -15,7 +19,11 @@ export default function ContentDrawer({
|
||||
closeDrawer,
|
||||
children,
|
||||
type,
|
||||
login,
|
||||
name,
|
||||
email,
|
||||
}: ContentDrawerProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const [width, setWidth] = useState<number | string>('30%');
|
||||
|
||||
@@ -30,6 +38,7 @@ export default function ContentDrawer({
|
||||
window.addEventListener('resize', calculateWidths);
|
||||
return () => window.removeEventListener('resize', calculateWidths);
|
||||
}, []);
|
||||
console.log(login, user?.login, login === user?.login);
|
||||
|
||||
const editDrawerTitle = (
|
||||
<div
|
||||
@@ -59,16 +68,21 @@ export default function ContentDrawer({
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
||||
<Avatar
|
||||
src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
|
||||
src={
|
||||
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||
}
|
||||
size={40}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text strong style={{ display: 'block' }}>
|
||||
Александр Александров
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{ display: 'block', fontSize: '20px' }}
|
||||
>
|
||||
{name} {login === user?.login ? t('you') : ''}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||
alexandralex@vorkout.ru
|
||||
{email}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +166,7 @@ export default function ContentDrawer({
|
||||
placement="right"
|
||||
open={open}
|
||||
width={width}
|
||||
destroyOnClose={true}
|
||||
destroyOnHidden={true}
|
||||
closable={false}
|
||||
>
|
||||
{children}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { Avatar } from 'antd';
|
||||
import Title from 'antd/es/typography/Title';
|
||||
import { useState } from 'react';
|
||||
import ContentDrawer from './ContentDrawer';
|
||||
import UserEdit from './UserEdit';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
@@ -7,6 +11,13 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export default function Header({ title, additionalContent }: HeaderProps) {
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
|
||||
const showEditDrawer = () => setOpenEdit(true);
|
||||
const closeEditDrawer = () => {
|
||||
setOpenEdit(false);
|
||||
};
|
||||
const user = useUserSelector();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -43,13 +54,24 @@ export default function Header({ title, additionalContent }: HeaderProps) {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={showEditDrawer}
|
||||
>
|
||||
<Avatar
|
||||
size={25.77}
|
||||
src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`}
|
||||
src={`https://gamma.heado.ru/go/ava?name=${user?.login}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContentDrawer
|
||||
login={user?.login}
|
||||
name={user?.name}
|
||||
email={user?.email}
|
||||
open={openEdit}
|
||||
closeDrawer={closeEditDrawer}
|
||||
type="edit"
|
||||
>
|
||||
{user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
|
||||
</ContentDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { Divider, Menu, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +14,7 @@ export default function SiderMenu({
|
||||
selectedKey,
|
||||
hangleMenuClick,
|
||||
}: SiderMenuProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const collapseStyle = collapsed
|
||||
? { fontSize: '12px' }
|
||||
@@ -74,7 +76,8 @@ export default function SiderMenu({
|
||||
label: t('settings'),
|
||||
className: 'no-expand-icon',
|
||||
children: [
|
||||
{
|
||||
user && (user.role === 'OWNER' || user.role === 'ADMIN')
|
||||
? {
|
||||
key: '/accounts',
|
||||
label: !collapsed ? (
|
||||
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
|
||||
@@ -82,7 +85,8 @@ export default function SiderMenu({
|
||||
t('accounts')
|
||||
),
|
||||
style: collapseStyle,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
key: '/events-list',
|
||||
label: !collapsed ? (
|
||||
|
@@ -8,13 +8,19 @@ import {
|
||||
UploadFile,
|
||||
GetProp,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
message,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUserSelector } from "@/store/userStore";
|
||||
import { UserCreate as NewUserCreate } from "@/types/user";
|
||||
import { UserService } from "@/services/userService";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
|
||||
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -24,10 +30,17 @@ const getBase64 = (file: FileType): Promise<string> =>
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
export default function UserCreate() {
|
||||
interface UserCreateProps {
|
||||
closeDrawer: () => void;
|
||||
getUsers: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [previewImage, setPreviewImage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
@@ -40,40 +53,49 @@ export default function UserCreate() {
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
||||
const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
|
||||
setFileList(newFileList);
|
||||
|
||||
const onFinish = async (values: NewUserCreate) => {
|
||||
setLoading(true);
|
||||
await UserService.createUser(values);
|
||||
await getUsers();
|
||||
closeDrawer();
|
||||
setLoading(false);
|
||||
message.info(t("createdAccountMessage"), 4);
|
||||
};
|
||||
|
||||
const customUploadButton = (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
height: '102px',
|
||||
width: '102px',
|
||||
backgroundColor: '#E2E2E2',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: "102px",
|
||||
width: "102px",
|
||||
backgroundColor: "#E2E2E2",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 8,
|
||||
marginTop: 30,
|
||||
cursor: 'pointer',
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/add_photo_alternate.svg"
|
||||
alt="add_photo_alternate"
|
||||
style={{ height: '18px', width: '18px' }}
|
||||
style={{ height: "18px", width: "18px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
||||
{t('selectPhoto')}
|
||||
<span style={{ fontSize: "14px", color: "#8c8c8c" }}>
|
||||
{t("selectPhoto")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const photoToUpload = (
|
||||
<div style={{ height: '102px' }}>
|
||||
<div style={{ height: "102px" }}>
|
||||
<Upload
|
||||
listType="picture-circle"
|
||||
fileList={fileList}
|
||||
@@ -85,11 +107,11 @@ export default function UserCreate() {
|
||||
</Upload>
|
||||
{previewImage && (
|
||||
<Image
|
||||
wrapperStyle={{ display: 'none' }}
|
||||
wrapperStyle={{ display: "none" }}
|
||||
preview={{
|
||||
visible: previewOpen,
|
||||
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||
afterOpenChange: (visible) => !visible && setPreviewImage(''),
|
||||
afterOpenChange: (visible) => !visible && setPreviewImage(""),
|
||||
}}
|
||||
src={previewImage}
|
||||
/>
|
||||
@@ -100,24 +122,24 @@ export default function UserCreate() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '36px',
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: "36px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{photoToUpload}
|
||||
@@ -126,83 +148,85 @@ export default function UserCreate() {
|
||||
<Form
|
||||
name="user-edit-form"
|
||||
layout="vertical"
|
||||
// onFinish={onFinish}
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
name: '',
|
||||
login: '',
|
||||
password: '',
|
||||
email: '',
|
||||
tenant: '',
|
||||
role: '',
|
||||
status: '',
|
||||
name: "",
|
||||
login: "",
|
||||
password: "",
|
||||
email: "",
|
||||
bindTenantId: "",
|
||||
role: "",
|
||||
status: "",
|
||||
}}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('name')}
|
||||
label={t("name")}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t('nameMessage') }]}
|
||||
rules={[{ required: true, message: t("nameMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('login')}
|
||||
label={t("login")}
|
||||
name="login"
|
||||
rules={[{ required: true, message: t('loginMessage') }]}
|
||||
rules={[{ required: true, message: t("loginMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('password')}
|
||||
label={t("password")}
|
||||
name="password"
|
||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
||||
rules={[{ message: t("passwordMessage") }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('email')}
|
||||
label={t("email")}
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: t('emailMessage') },
|
||||
{ type: 'email', message: t('emailErrorMessage') },
|
||||
{ message: t("emailMessage") },
|
||||
{ type: "email", message: t("emailErrorMessage") },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('tenant')}
|
||||
name="tenant"
|
||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||
label={t("tenant")}
|
||||
name="bindTenantId"
|
||||
rules={[{ message: t("tenantMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('role')}
|
||||
label={t("role")}
|
||||
name="role"
|
||||
rules={[{ required: true, message: t('roleMessage') }]}
|
||||
rules={[{ required: true, message: t("roleMessage") }]}
|
||||
>
|
||||
<Select placeholder={t('roleMessage')}>
|
||||
<Option value="Директор магазина">Директор магазина</Option>
|
||||
<Option value="Менеджер">Менеджер</Option>
|
||||
<Option value="Кассир">Кассир</Option>
|
||||
<Select placeholder={t("roleMessage")}>
|
||||
{user && user.role === "OWNER" ? (
|
||||
<Option value="ADMIN">{t("ADMIN")}</Option>
|
||||
) : undefined}
|
||||
<Option value="EDITOR">{t("EDITOR")}</Option>
|
||||
<Option value="VIEWER">{t("VIEWER")}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('status')}
|
||||
label={t("status")}
|
||||
name="status"
|
||||
rules={[{ required: true, message: t('statusMessage') }]}
|
||||
rules={[{ required: true, message: t("statusMessage") }]}
|
||||
>
|
||||
<Select placeholder={t('statusMessage')}>
|
||||
<Option value="ACTIVE">Активен</Option>
|
||||
<Option value="DISABLED">Неактивен</Option>
|
||||
<Option value="BLOCKED">Заблокирован</Option>
|
||||
<Option value="DELETED">Удален</Option>
|
||||
<Select placeholder={t("statusMessage")}>
|
||||
<Option value="ACTIVE">{t("ACTIVE")}</Option>
|
||||
<Option value="DISABLED">{t("DISABLED")}</Option>
|
||||
<Option value="BLOCKED">{t("BLOCKED")}</Option>
|
||||
<Option value="DELETED">{t("DELETED")}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@@ -213,14 +237,23 @@ export default function UserCreate() {
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
style={{ color: '#000' }}
|
||||
style={{ color: "#000" }}
|
||||
>
|
||||
{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')}
|
||||
style={{ height: "18px", width: "18px" }}
|
||||
/>{" "}
|
||||
{t("addAccount")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
@@ -1,47 +1,101 @@
|
||||
import { Button, Form, Input, Select } from 'antd';
|
||||
import { UserService } from '@/services/userService';
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { UserUpdate } from '@/types/user';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, message, Select, Spin } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function UserEdit() {
|
||||
interface UserEditProps {
|
||||
userId?: number;
|
||||
closeDrawer: () => void;
|
||||
}
|
||||
|
||||
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
|
||||
const currentUser = useUserSelector();
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useState<UserUpdate>({
|
||||
id: 0,
|
||||
name: '',
|
||||
login: '',
|
||||
email: '',
|
||||
password: '',
|
||||
bindTenantId: '',
|
||||
role: 'VIEWER',
|
||||
meta: {},
|
||||
createdAt: '',
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
if (typeof userId === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const user = await UserService.getUserById(userId);
|
||||
setUser(user);
|
||||
form.setFieldsValue({ ...user });
|
||||
}
|
||||
|
||||
getUser();
|
||||
}, []);
|
||||
|
||||
const onFinish = async (values: UserUpdate) => {
|
||||
setLoading(true);
|
||||
let updatedUser: Partial<UserUpdate> = {};
|
||||
|
||||
(Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
|
||||
if (values[key] !== user[key]) {
|
||||
updatedUser = { ...updatedUser, [key]: values[key] };
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(updatedUser).length > 0) {
|
||||
console.log('updateUser', userId, updatedUser);
|
||||
await UserService.updateUser(userId!, updatedUser);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
message.info(t('editAccountMessage'), 4);
|
||||
closeDrawer();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Form
|
||||
form={form}
|
||||
name="user-edit-form"
|
||||
layout="vertical"
|
||||
// onFinish={onFinish}
|
||||
initialValues={{
|
||||
name: 'Александр Александров',
|
||||
login: 'alexandralex@vorkout.ru',
|
||||
password: 'jKUUl776GHd',
|
||||
email: 'alexandralex@vorkout.ru',
|
||||
tenant: 'text',
|
||||
role: 'Директор магазина',
|
||||
status: 'Активен',
|
||||
}}
|
||||
onFinish={onFinish}
|
||||
initialValues={{ ...user }}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('name')}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t('nameMessage') }]}
|
||||
rules={[{ message: t('nameMessage') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{user?.id === currentUser?.id ? undefined : (
|
||||
<Form.Item
|
||||
label={t('login')}
|
||||
name="login"
|
||||
rules={[{ required: true, message: t('loginMessage') }]}
|
||||
rules={[{ message: t('loginMessage') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('password')}
|
||||
name="password"
|
||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
||||
rules={[{ message: t('passwordMessage') }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
@@ -59,23 +113,27 @@ export default function UserEdit() {
|
||||
|
||||
<Form.Item
|
||||
label={t('tenant')}
|
||||
name="tenant"
|
||||
name="bindTenantId"
|
||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
{user?.id === currentUser?.id ? undefined : (
|
||||
<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>
|
||||
{currentUser && currentUser.role === 'OWNER' ? (
|
||||
<Option value="ADMIN">{t('ADMIN')}</Option>
|
||||
) : undefined}
|
||||
<Option value="EDITOR">{t('EDITOR')}</Option>
|
||||
<Option value="VIEWER">{t('VIEWER')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('status')}
|
||||
@@ -83,10 +141,10 @@ export default function UserEdit() {
|
||||
rules={[{ required: true, message: t('statusMessage') }]}
|
||||
>
|
||||
<Select placeholder={t('statusMessage')}>
|
||||
<Option value="ACTIVE">Активен</Option>
|
||||
<Option value="DISABLED">Неактивен</Option>
|
||||
<Option value="BLOCKED">Заблокирован</Option>
|
||||
<Option value="DELETED">Удален</Option>
|
||||
<Option value="ACTIVE">{t('ACTIVE')}</Option>
|
||||
<Option value="DISABLED">{t('DISABLED')}</Option>
|
||||
<Option value="BLOCKED">{t('BLOCKED')}</Option>
|
||||
<Option value="DELETED">{t('DELETED')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@@ -99,12 +157,21 @@ export default function UserEdit() {
|
||||
block
|
||||
style={{ color: '#000' }}
|
||||
>
|
||||
{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>
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import './i18n';
|
||||
import '@/config/i18n';
|
||||
import { ConfigProvider } from 'antd';
|
||||
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 ru from 'antd/locale/ru_RU';
|
||||
@@ -18,7 +19,7 @@ export default function AppWrapper({ children }: any) {
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
|
||||
{children}
|
||||
<BrowserRouter>{children}</BrowserRouter>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
@@ -37,6 +37,20 @@ i18n
|
||||
addAccount: 'Add account',
|
||||
save: 'Save changes',
|
||||
newAccount: 'New account',
|
||||
ACTIVE: 'Active',
|
||||
DISABLED: 'Disabled',
|
||||
BLOCKED: 'Blocked',
|
||||
DELETED: 'Deleted',
|
||||
OWNER: 'Owner',
|
||||
ADMIN: 'Admin',
|
||||
EDITOR: 'Editor',
|
||||
VIEWER: 'Viewer',
|
||||
nameLogin: 'Name, login',
|
||||
createdAt: 'Created',
|
||||
saving: 'Saving...',
|
||||
createdAccountMessage: 'User successfully created!',
|
||||
editAccountMessage: 'User successfully updated!',
|
||||
you: '(You)',
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
@@ -66,6 +80,20 @@ i18n
|
||||
addAccount: 'Добавить аккаунт',
|
||||
save: 'Сохранить изменения',
|
||||
newAccount: 'Новая учетная запись',
|
||||
ACTIVE: 'Активен',
|
||||
DISABLED: 'Выключен',
|
||||
BLOCKED: 'Заблокирован',
|
||||
DELETED: 'Удален',
|
||||
OWNER: 'Владелец',
|
||||
ADMIN: 'Админ',
|
||||
EDITOR: 'Редактор',
|
||||
VIEWER: 'Наблюдатель',
|
||||
nameLogin: 'Имя, Логин',
|
||||
createdAt: 'Создано',
|
||||
saving: 'Сохранение...',
|
||||
createdAccountMessage: 'Пользователь успешно создан!',
|
||||
editAccountMessage: 'Пользователь успешно обновлен!',
|
||||
you: '(Вы)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
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,9 +1,8 @@
|
||||
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';
|
||||
import '@/index.css';
|
||||
import App from '@/App';
|
||||
import AppWrapper from '@/config/AppWrapper';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@@ -11,8 +10,6 @@ const root = ReactDOM.createRoot(
|
||||
|
||||
root.render(
|
||||
<AppWrapper>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</AppWrapper>
|
||||
);
|
@@ -1,42 +1,219 @@
|
||||
import Header from '../components/Header';
|
||||
import { useState } from 'react';
|
||||
import ContentDrawer from '../components/ContentDrawer';
|
||||
import UserCreate from '../components/UserCreate';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import { UserService } from '../services/userService';
|
||||
import { User } from '../types/user';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
|
||||
import Header from "@/components/Header";
|
||||
import ContentDrawer from "@/components/ContentDrawer";
|
||||
import UserCreate from "@/components/UserCreate";
|
||||
import { Avatar, Table } from "antd";
|
||||
import { TableProps } from "antd/lib";
|
||||
import { UserService } from "@/services/userService";
|
||||
import UserEdit from "@/components/UserEdit";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export default function AccountsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const showDrawer = () => setOpen(true);
|
||||
const closeDrawer = () => setOpen(false);
|
||||
const [activeAccount, setActiveAccount] = useState<
|
||||
{ login: string; id: number; name: string; email: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
const [accounts, setAccounts] = useState<User[]>([]);
|
||||
const showCreateDrawer = () => setOpenCreate(true);
|
||||
const closeCreateDrawer = () => {
|
||||
setActiveAccount(undefined);
|
||||
setOpenCreate(false);
|
||||
};
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
|
||||
const showEditDrawer = () => setOpenEdit(true);
|
||||
const closeEditDrawer = () => {
|
||||
setActiveAccount(undefined);
|
||||
setOpenEdit(false);
|
||||
};
|
||||
|
||||
const [accounts, setAccounts] = useState<AllUserResponse>({
|
||||
amountCount: 0,
|
||||
amountPages: 0,
|
||||
users: [],
|
||||
currentPage: 1,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
async function getUsers() {
|
||||
const page = Number(searchParams.get("page") || "1");
|
||||
const limit = Number(searchParams.get("limit") || "10");
|
||||
setSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
const data = await UserService.getUsers(page, limit);
|
||||
console.log("searchParams", searchParams);
|
||||
setAccounts(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, []);
|
||||
|
||||
const statusColor = {
|
||||
ACTIVE: "#27AE60",
|
||||
DISABLED: "#606060",
|
||||
BLOCKED: "#FF0000",
|
||||
DELETED: "#B30000",
|
||||
};
|
||||
|
||||
const columns: TableProps<AllUser>["columns"] = [
|
||||
{
|
||||
title: "#",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: t("nameLogin"),
|
||||
dataIndex: "nameLogin",
|
||||
key: "nameLogin",
|
||||
render: (text, record) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveAccount({
|
||||
login: record.login,
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
email: record.email || "",
|
||||
});
|
||||
showEditDrawer();
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "32px",
|
||||
width: "32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
src={`https://gamma.heado.ru/go/ava?name=${record.login}`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div>{record.name}</div>
|
||||
<div style={{ color: "#606060" }}>{record.login}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "E-mail",
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: t("tenant"),
|
||||
dataIndex: "bindTenantId",
|
||||
key: "tenant",
|
||||
},
|
||||
{
|
||||
title: t("role"),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
render: (text) => <div>{t(text)}</div>,
|
||||
},
|
||||
{
|
||||
title: t("createdAt"),
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
render: (text) => (
|
||||
<div>
|
||||
{new Date(text).toLocaleString("ru", {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (text) => (
|
||||
<div style={{ color: statusColor[text as AccountStatus] }}>
|
||||
{t(text)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onTableChange: TableProps<AllUser>["onChange"] = (pagination) => {
|
||||
console.log(pagination);
|
||||
UserService.getUsers(
|
||||
pagination.current as number,
|
||||
pagination.pageSize
|
||||
).then((data) => {
|
||||
setAccounts(data);
|
||||
setSearchParams({
|
||||
page: data.currentPage.toString(),
|
||||
limit: data.limit.toString(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={t('accounts')}
|
||||
title={t("accounts")}
|
||||
additionalContent={
|
||||
<img
|
||||
src="./icons/header/add_2.svg"
|
||||
alt="add"
|
||||
style={{
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
cursor: 'pointer',
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={showDrawer}
|
||||
onClick={showCreateDrawer}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Table
|
||||
size="small"
|
||||
onChange={onTableChange}
|
||||
columns={columns}
|
||||
dataSource={accounts.users}
|
||||
pagination={{
|
||||
pageSize: accounts.limit,
|
||||
current: accounts.currentPage,
|
||||
total: accounts.amountCount,
|
||||
}}
|
||||
rowKey={"id"}
|
||||
/>
|
||||
|
||||
<ContentDrawer open={open} closeDrawer={closeDrawer} type="create">
|
||||
<UserCreate />
|
||||
<ContentDrawer
|
||||
open={openCreate}
|
||||
closeDrawer={closeCreateDrawer}
|
||||
type="create"
|
||||
>
|
||||
<UserCreate getUsers={getUsers} closeDrawer={closeCreateDrawer} />
|
||||
</ContentDrawer>
|
||||
<ContentDrawer
|
||||
login={activeAccount?.login}
|
||||
name={activeAccount?.name}
|
||||
email={activeAccount?.email}
|
||||
open={openEdit}
|
||||
closeDrawer={closeEditDrawer}
|
||||
type="edit"
|
||||
>
|
||||
<UserEdit userId={activeAccount?.id} closeDrawer={closeEditDrawer} />
|
||||
</ContentDrawer>
|
||||
</>
|
||||
);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import Header from '@/components/Header';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Header from '../components/Header';
|
||||
|
||||
export default function ConfigurationPage() {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import Header from '@/components/Header';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Header from '../components/Header';
|
||||
|
||||
export default function EventsListPage() {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Typography } from 'antd';
|
||||
import { Form, Input, Button, Typography, message } from 'antd';
|
||||
import {
|
||||
EyeInvisibleOutlined,
|
||||
EyeTwoTone,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { Auth } from '../types/auth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { AuthService } from '@/services/authService';
|
||||
import { Auth } from '@/types/auth';
|
||||
|
||||
const { Text, Link } = Typography;
|
||||
|
||||
@@ -45,7 +45,11 @@ export default function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form name="login" onFinish={onFinish} layout="vertical">
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{ required: true, message: 'Введите login' }]}
|
||||
|
@@ -2,8 +2,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import Sider from 'antd/es/layout/Sider';
|
||||
import SiderMenu from '../components/SiderMenu';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import SiderMenu from '@/components/SiderMenu';
|
||||
import ProcessDiagramPage from './ProcessDiagramPage';
|
||||
import RunningProcessesPage from './RunningProcessesPage';
|
||||
import AccountsPage from './AccountsPage';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import Header from '@/components/Header';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Header from '../components/Header';
|
||||
|
||||
export default function ProcessDiagramPage() {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// ProtectedRoute.js
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useUserSelector } from '../store/userStore';
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
|
||||
const ProtectedRoute = (): React.JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import Header from '@/components/Header';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Header from '../components/Header';
|
||||
|
||||
export default function RunningProcessesPage() {
|
||||
const { t } = useTranslation();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import api from '../api/api';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useUserStore } from '../store/userStore';
|
||||
import { Auth } from '../types/auth';
|
||||
import { UserService } from './userService';
|
||||
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) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import api from '../api/api';
|
||||
import { User } from '../types/user';
|
||||
import api from '@/api/api';
|
||||
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
|
||||
|
||||
export class UserService {
|
||||
static async getProfile(): Promise<User> {
|
||||
@@ -9,8 +9,30 @@ export class UserService {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
|
||||
const users = api.getUsers(page, limit);
|
||||
return users;
|
||||
static async getUsers(
|
||||
page: number = 1,
|
||||
limit: number = 10
|
||||
): Promise<AllUserResponse> {
|
||||
console.log('getUsers');
|
||||
const allUsers = api.getUsers(page, limit);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
static async getUserById(userId: number): Promise<User> {
|
||||
console.log('getUserById');
|
||||
const user = api.getUserById(userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
static async createUser(user: UserCreate): Promise<User> {
|
||||
console.log('createUser');
|
||||
const createdUser = api.createUser(user);
|
||||
return createdUser;
|
||||
}
|
||||
|
||||
static async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||
console.log('updateUser');
|
||||
const updatedUser = api.updateUser(userId, user);
|
||||
return updatedUser;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { User } from '@/types/user';
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { User } from '../types/user';
|
||||
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
|
||||
|
@@ -191,6 +191,10 @@ export interface components {
|
||||
amountCount: number;
|
||||
/** Amountpages */
|
||||
amountPages: number;
|
||||
/** Currentpage */
|
||||
currentPage: number;
|
||||
/** Limit */
|
||||
limit: number;
|
||||
};
|
||||
/** Auth */
|
||||
Auth: {
|
||||
@@ -247,6 +251,25 @@ export interface components {
|
||||
createdAt: string;
|
||||
status: components["schemas"]["AccountStatus"];
|
||||
};
|
||||
/** UserCreate */
|
||||
UserCreate: {
|
||||
/** Name */
|
||||
name?: string | null;
|
||||
/** Login */
|
||||
login?: string | null;
|
||||
/** Email */
|
||||
email?: string | null;
|
||||
/** Password */
|
||||
password?: string | null;
|
||||
/** Bindtenantid */
|
||||
bindTenantId?: string | null;
|
||||
role?: components["schemas"]["AccountRole"] | null;
|
||||
/** Meta */
|
||||
meta?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
status?: components["schemas"]["AccountStatus"] | null;
|
||||
};
|
||||
/** UserUpdate */
|
||||
UserUpdate: {
|
||||
/** Id */
|
||||
@@ -257,6 +280,8 @@ export interface components {
|
||||
login?: string | null;
|
||||
/** Email */
|
||||
email?: string | null;
|
||||
/** Password */
|
||||
password?: string | null;
|
||||
/** Bindtenantid */
|
||||
bindTenantId?: string | null;
|
||||
role?: components["schemas"]["AccountRole"] | null;
|
||||
@@ -435,7 +460,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
"application/json": components["schemas"]["UserCreate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@@ -445,7 +470,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["AllUser"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -476,7 +501,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@@ -511,7 +536,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
@@ -1,3 +1,9 @@
|
||||
import { components } from "./openapi-types"
|
||||
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'];
|
||||
|
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -18,9 +14,11 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./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]
|
||||
environment=
|
||||
REACT_APP_WEBSOCKET_PROTOCOL=ws,
|
||||
REACT_APP_HTTP_PROTOCOL=http,
|
||||
REACT_APP_API_URL=localhost:8000,
|
||||
REACT_APP_URL=localhost:3000
|
||||
command=bash -c 'cd client; npm run build; serve -s build'
|
||||
VITE_APP_WEBSOCKET_PROTOCOL=ws,
|
||||
VITE_APP_HTTP_PROTOCOL=http,
|
||||
VITE_APP_API_URL=localhost:8000,
|
||||
VITE_APP_URL=localhost:3000
|
||||
command=bash -c 'cd client; npm run build; npm run preview'
|
||||
numprocs=1
|
||||
process_name=node-%(process_num)d
|
||||
stdout_logfile=client.out.log
|
||||
|
Reference in New Issue
Block a user