Compare commits

...

76 Commits

Author SHA1 Message Date
e6589a0950 chore: update client and api patch version to 0.0.5 2025-07-02 12:25:11 +05:00
a0fcc324fa Merge pull request 'VORKOUT-8' (#13) from VORKOUT-8 into master
Reviewed-on: #13
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
2025-07-02 12:23:43 +05:00
vsyroc
2a35386c1d refactor(api): format with ruff 2025-07-02 12:23:22 +05:00
a936500101 refactor(client): remove required fileds 2025-07-01 13:35:40 +05:00
4c0beb24f9 fix(api): on duplicate password update 2025-06-30 17:52:31 +05:00
ad1369c3e3 refactor(api): change UserCreate model fields 2025-06-30 16:30:53 +05:00
5958f29ba8 fix(AccountPage): fix empty string in search params 2025-06-30 14:02:53 +05:00
784be40369 feat(AccountsPage): update accounts list after create user 2025-06-30 12:37:45 +05:00
ad312d4ff8 feat(AccountsPage): loading with search params 2025-06-30 12:19:29 +05:00
ba65f36696 feat(AccountsPage): add page and limit to search params 2025-06-27 16:41:48 +05:00
ad0a4837fc feat(api): add update password 2025-06-27 13:31:32 +05:00
1eadd834e3 feat(client): add update password 2025-06-27 13:25:25 +05:00
8f3fde623f refactor(api): move create password 2025-06-27 12:25:25 +05:00
edfd2c1159 refactor(Makefile): fix alembic 2025-06-26 16:38:57 +05:00
3d8ee4835d refactor(db): increase account_keyring_table.key_value size 2025-06-26 16:38:26 +05:00
9c9201f130 feat(client): add userEdit 2025-06-26 16:15:03 +05:00
0eed0b0f20 refactor(clint-UserEdit): remove login and role on self user 2025-06-26 15:36:45 +05:00
7127d88524 feat(client-Header): add userEdit on header 2025-06-26 15:26:49 +05:00
692461e266 feat: create new user with password 2025-06-26 15:08:05 +05:00
22064d2b52 feat(types): generate new types 2025-06-26 15:04:43 +05:00
febac9659f refactor(Makefile): change source with poetry 2025-06-26 12:27:14 +05:00
6c0a6ac1d4 feat(AccountsPage): add change page size 2025-06-25 21:01:19 +05:00
a7e813b447 feat(UserCreate): add loading animation 2025-06-25 14:09:51 +05:00
919758ab69 fix: bind tenant id 2025-06-25 13:54:48 +05:00
53bf173373 fix(api): fix get_user_by_id method 2025-06-25 13:39:16 +05:00
8f5dd07bf5 feat(client): add create user 2025-06-25 13:38:43 +05:00
448e4264a5 feat(AccountPage): add destroyOnHidden to ContentDrawer and fix tenant and login 2025-06-24 16:30:13 +05:00
e5dfdc3464 feat(AccountsPage): add userEdit 2025-06-24 16:23:43 +05:00
aae56a8c73 feat(AccountsPage): add on table change 2025-06-24 13:19:17 +05:00
7c2c4071cc feat(Makefile): add regenerate-openapi-local command 2025-06-24 13:17:28 +05:00
18bb79262c feat(api): add current page to AllUserResponse and fix returning type 2025-06-24 13:12:31 +05:00
a3ee18f6fd feat: add accounts table 2025-06-24 13:00:40 +05:00
71ab39a03c chore: update client patch version to 0.0.4 2025-06-24 12:56:57 +05:00
5d39065a7f Merge pull request 'VORKOUT-15' (#12) from VORKOUT-15 into master
Reviewed-on: #12
2025-06-24 12:56:16 +05:00
92ff1d3d0a refactor: Makefile 2025-06-23 13:08:07 +05:00
5ed8ca9251 chore(client): update sctucture after switching to vite 2025-06-23 13:07:51 +05:00
8ac329e76e feat: add readme 2025-06-23 13:05:13 +05:00
c68b512902 chore: update client patch version to 0.0.3 and api patch version to 0.0.4 2025-06-16 16:38:22 +05:00
70aaaeabf1 Merge pull request 'VORKOUT-6' (#11) from VORKOUT-6 into master
Reviewed-on: #11
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
2025-06-16 16:34:24 +05:00
92203351ff feat(client): regenerate openapi types 2025-06-16 12:56:30 +05:00
ee92428ec3 refactor(api): refactor refresh logic 2025-06-16 12:46:14 +05:00
c87581c9e2 feat(client): add auth logic 2025-06-16 12:35:02 +05:00
79cb434ebd feat: add stores 2025-06-16 12:32:45 +05:00
5def1a9bb1 feat: add authService and rename userService 2025-06-16 12:32:08 +05:00
d55a99aafd feat: add login page and auth logic 2025-06-10 17:50:28 +05:00
599bf22bda feat: add axios-retry 2025-06-10 17:48:57 +05:00
e114f963ab refactor: middleware and refresh 2025-06-10 17:47:46 +05:00
d7a5109d8e feat: add user store 2025-06-09 17:47:45 +05:00
8122f1878a feat: add zustand 2025-06-09 17:47:26 +05:00
9d2aef5671 feat: add user and auth types 2025-06-09 17:00:27 +05:00
34be97996e feat: add axios instance 2025-06-09 17:00:01 +05:00
e0dca78ef3 feat: add login page 2025-06-09 16:59:31 +05:00
0bba6e7f54 feat: add axios 2025-06-09 16:58:47 +05:00
21216a6ad5 feat: generate types from openapi 2025-06-09 14:32:44 +05:00
2bf0f20e73 Merge pull request 'VORKOUT-7 : fix' (#10) from VORKOUT-7fix into master
Reviewed-on: #10
Reviewed-by: Vladislav Syrochkin <vlad.dev@heado.ru>
2025-06-09 13:06:37 +05:00
abd87b46b3 refactor(tables): change enum, str to StrEnum 2025-06-09 13:03:33 +05:00
787dc0e8f8 refactor: refactor api project with ruff 2025-06-09 12:12:48 +05:00
2e4e9d1113 feat: add bearer schema to all endpoints with auth 2025-06-09 12:10:40 +05:00
c1d315d6e9 feat: add bearer schema and get_current_user function 2025-06-09 11:51:13 +05:00
TheNoxium
a31758192d fix: model 2025-06-09 02:10:46 +05:00
8965365afc Merge pull request 'VORKOUT-7' (#9) from VORKOUT-7 into master
Reviewed-on: #9
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
2025-06-05 16:29:14 +05:00
TheNoxium
c68286f7cc fix:merge error 2025-06-05 14:55:38 +05:00
TheNoxium
58cc23f79b fix: mrege error 2025-06-05 14:41:18 +05:00
TheNoxium
48d52bf903 Merge branch 'master' into VORKOUT-7 2025-06-05 13:04:25 +05:00
TheNoxium
66c60cfc59 fix: name 2025-06-04 12:03:59 +05:00
TheNoxium
c60d19262e fix: model, name 2025-06-03 13:01:10 +05:00
TheNoxium
31236d558f feat: model update 2025-06-02 15:56:44 +05:00
TheNoxium
5094b84675 fix: TypeAdapter 2025-05-30 08:51:49 +05:00
7e1fe6f5c4 fix(api): fix pyproject.toml and poetry.lock files 2025-05-29 15:25:03 +05:00
TheNoxium
320c13183f feat: add TypeAdapter 2025-05-28 14:10:00 +05:00
TheNoxium
98a4692247 feat: pydantic models for swagger 2025-05-26 13:45:40 +05:00
8613cbdaac chore: update the patch version to 0.0.3 2025-05-23 18:08:32 +05:00
5f981e8ce1 Merge pull request 'VORKOUT-13' (#8) from VORKOUT-13 into master
Reviewed-on: #8
Reviewed-by: cyrussmeat <dr.cyrill@gmail.com>
2025-05-23 18:07:39 +05:00
TheNoxium
96dbc744d7 feat: add query params 2025-05-23 12:48:09 +05:00
TheNoxium
e47e449a36 feat: add page, limit 2025-05-23 12:41:32 +05:00
TheNoxium
c3c421f66f feat: get all users 2025-05-21 14:58:06 +05:00
65 changed files with 4825 additions and 16064 deletions

View File

@ -12,36 +12,35 @@ services:
start-api: start-api:
cd api && \ cd api && \
source .venv/bin/activate && \ poetry run python -m ${API_APPLICATION_NAME}
python -m ${API_APPLICATION_NAME}
start-client: start-client:
cd client && \ cd client && \
npm start npm run dev
migrate: migrate:
cd api && \ cd api && \
source .venv/bin/activate && \
cd $(API_APPLICATION_NAME)/db && \ cd $(API_APPLICATION_NAME)/db && \
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args) PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic upgrade $(args)
downgrade: downgrade:
cd api && \ cd api && \
source .venv/bin/activate && \
cd $(API_APPLICATION_NAME)/db && \ cd $(API_APPLICATION_NAME)/db && \
PYTHONPATH='../..' alembic downgrade -1 PYTHONPATH='../..' poetry run alembic downgrade -1
revision: revision:
cd api && \ cd api && \
source .venv/bin/activate && \
cd $(API_APPLICATION_NAME)/db && \ cd $(API_APPLICATION_NAME)/db && \
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic revision --autogenerate
venv-api: venv-api:
cd api && \ cd api && \
poetry env activate \
poetry install poetry install
venv-client:
cd client && \
npm install
install: install:
make migrate head && \ make migrate head && \
cd api && \ cd api && \
@ -58,3 +57,8 @@ format-api:
check-api: check-api:
cd api && \ cd api && \
poetry run ruff format . --check poetry run ruff format . --check
regenerate-openapi-local:
cd client \
rm src/types/openapi-types.ts \
npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts

View File

@ -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",
)
```

View File

@ -73,6 +73,7 @@ if __name__ == "__main__":
log_level="info", log_level="info",
) )
app.add_middleware(MiddlewareAccessTokenValidadtion)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=origins, allow_origins=origins,
@ -80,5 +81,3 @@ app.add_middleware(
allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"], allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"],
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(MiddlewareAccessTokenValidadtion)

View 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 ###

View File

@ -1,42 +1,75 @@
import math
from datetime import datetime, timezone
from enum import Enum
from typing import Optional from typing import Optional
from datetime import datetime, timezone from sqlalchemy import func, insert, select
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from enum import Enum
from api.db.tables.account import account_table from api.db.tables.account import account_table
from api.schemas.account.account import User from api.schemas.account.account import User
from api.schemas.endpoints.account import UserUpdate, Role, Status from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate
async def get_user_id(connection: AsyncConnection, id: int) -> Optional[User]: async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]:
"""
Получает список ползовелей заданных значениями page, limit.
"""
first_user = page * limit - (limit)
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.created_at,
account_table.c.status,
)
.order_by(account_table.c.id)
.offset(first_user)
.limit(limit)
)
count_query = select(func.count()).select_from(account_table)
result = await connection.execute(query)
count_result = await connection.execute(count_query)
users_data = result.mappings().all()
total_count = count_result.scalar()
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,
current_page=page,
limit=limit,
)
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
""" """
Получает юзера по id. Получает юзера по id.
""" """
query = select(account_table).where(account_table.c.id == id) query = select(account_table).where(account_table.c.id == user_id)
user_db_cursor = await connection.execute(query) user_db_cursor = await connection.execute(query)
user_db = user_db_cursor.one_or_none() user = user_db_cursor.mappings().one_or_none()
if not user_db: if not user:
return None return None
user_data = { return AllUser.model_validate(user)
column.name: (
getattr(user_db, column.name).name
if isinstance(getattr(user_db, column.name), Enum)
else getattr(user_db, column.name)
)
for column in account_table.columns
}
return User.model_validate(user_data)
async def get_user_login(connection: AsyncConnection, login: str) -> Optional[User]: async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
""" """
Получает юзера по login. Получает юзера по login.
""" """
@ -60,7 +93,7 @@ async def get_user_login(connection: AsyncConnection, login: str) -> Optional[Us
return User.model_validate(user_data) return User.model_validate(user_data)
async def update_user_id(connection: AsyncConnection, update_values, user) -> Optional[User]: async def update_user_by_id(connection: AsyncConnection, update_values, user) -> Optional[User]:
""" """
Вносит изменеия в нужное поле таблицы account_table. Вносит изменеия в нужное поле таблицы account_table.
""" """
@ -69,7 +102,7 @@ async def update_user_id(connection: AsyncConnection, update_values, user) -> Op
await connection.commit() await connection.commit()
async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]: async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
""" """
Создает нове поле в таблице account_table. Создает нове поле в таблице account_table.
""" """
@ -79,14 +112,15 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int)
email=user.email, email=user.email,
bind_tenant_id=user.bind_tenant_id, bind_tenant_id=user.bind_tenant_id,
role=user.role.value, role=user.role.value,
meta=user.meta, meta=user.meta or {},
creator_id=creator_id, creator_id=creator_id,
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
status=user.status.value, status=user.status.value,
) )
await connection.execute(query) res = await connection.execute(query)
await connection.commit() await connection.commit()
new_user = await get_user_by_id(connection, res.lastrowid)
return user return new_user

View File

@ -8,13 +8,14 @@ from api.db.tables.account import account_table, account_keyring_table, KeyType,
from api.schemas.account.account import User from api.schemas.account.account import User
from api.schemas.account.account_keyring import AccountKeyring from api.schemas.account.account_keyring import AccountKeyring
from api.schemas.endpoints.account import AllUser
from api.utils.key_id_gen import KeyIdGenerator from api.utils.key_id_gen import KeyIdGenerator
from datetime import datetime, timezone from datetime import datetime, timezone
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]: async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
query = ( query = (
select(account_table, account_keyring_table) select(account_table, account_keyring_table)
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id) .join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
@ -45,18 +46,17 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
for column in account_keyring_table.columns for column in account_keyring_table.columns
} }
user = User.model_validate(user_data) user = AllUser.model_validate(user_data)
password = AccountKeyring.model_validate(password_data) password = AccountKeyring.model_validate(password_data)
return user, password return user, password
async def upgrade_old_refresh_token(connection: AsyncConnection, user, refresh_token) -> Optional[User]: async def upgrade_old_refresh_token(connection: AsyncConnection, refresh_token) -> Optional[User]:
new_status = KeyStatus.EXPIRED new_status = KeyStatus.EXPIRED
update_query = ( update_query = (
update(account_keyring_table) update(account_keyring_table)
.where( .where(
account_table.c.id == user.id,
account_keyring_table.c.status == KeyStatus.ACTIVE, account_keyring_table.c.status == KeyStatus.ACTIVE,
account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN, account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN,
account_keyring_table.c.key_value == refresh_token, account_keyring_table.c.key_value == refresh_token,

View File

@ -1,17 +1,17 @@
from typing import Optional from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import Optional
from sqlalchemy import insert, select from sqlalchemy import insert, select, update
from sqlalchemy.dialects.mysql import insert as mysql_insert
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.tables.account import account_keyring_table from api.db.tables.account import account_keyring_table, KeyStatus, KeyType
from api.schemas.account.account_keyring import AccountKeyring from api.schemas.account.account_keyring import AccountKeyring
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate, StatusKey, TypeKey from api.utils.hasher import hasher
async def get_key_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]: async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
""" """
Получает key по key_id. Получает key по key_id.
""" """
@ -35,7 +35,7 @@ async def get_key_id(connection: AsyncConnection, key_id: str) -> Optional[Accou
return AccountKeyring.model_validate(user_data) return AccountKeyring.model_validate(user_data)
async def update_key_id(connection: AsyncConnection, update_values, key) -> Optional[AccountKeyring]: async def update_key_by_id(connection: AsyncConnection, update_values, key) -> Optional[AccountKeyring]:
""" """
Вносит изменеия в нужное поле таблицы account_keyring_table. Вносит изменеия в нужное поле таблицы account_keyring_table.
""" """
@ -68,3 +68,37 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i
await connection.commit() await connection.commit()
return key return key
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
if password is None:
password = hasher.generate_password()
hashed_password = hasher.hash_data(password)
stmt = mysql_insert(account_keyring_table).values(
owner_id=owner_id,
key_type=KeyType.PASSWORD.value,
key_id="PASSWORD",
key_value=hashed_password,
created_at=datetime.now(timezone.utc),
expiry=datetime.now(timezone.utc) + timedelta(days=365),
status=KeyStatus.ACTIVE,
)
stmt.on_duplicate_key_update(key_value=hashed_password)
await connection.execute(stmt)
await connection.commit()
async def update_password_key(connection: AsyncConnection, owner_id: int, password: str):
stmt = select(account_keyring_table).where(account_keyring_table.c.owner_id == owner_id)
result = await connection.execute(stmt)
keyring = result.one_or_none()
if not keyring:
await create_password_key(connection, password, owner_id)
else:
stmt = (
update(account_keyring_table)
.values(key_value=hasher.hash_data(password), expiry=datetime.now(timezone.utc) + timedelta(days=365))
.where(account_keyring_table.c.owner_id == owner_id)
)
await connection.execute(stmt)
await connection.commit()

View File

@ -1,24 +1,26 @@
import enum
from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index from sqlalchemy import Table, Column, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func from sqlalchemy.sql import func
from enum import Enum, auto from enum import Enum
from api.db.sql_types import UnsignedInt from api.db.sql_types import UnsignedInt
from api.db import metadata from api.db import metadata
class AccountRole(str, Enum): class AccountRole(enum.StrEnum):
OWNER = auto() OWNER = "OWNER"
ADMIN = auto() ADMIN = "ADMIN"
EDITOR = auto() EDITOR = "EDITOR"
VIEWER = auto() VIEWER = "VIEWER"
class AccountStatus(str, Enum): class AccountStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
DISABLED = auto() DISABLED = "DISABLED"
BLOCKED = auto() BLOCKED = "BLOCKED"
DELETED = auto() DELETED = "DELETED"
account_table = Table( account_table = Table(
@ -39,17 +41,17 @@ account_table = Table(
) )
class KeyType(str, Enum): class KeyType(enum.StrEnum):
PASSWORD = auto() PASSWORD = "PASSWORD"
ACCESS_TOKEN = auto() ACCESS_TOKEN = "ACCESS_TOKEN"
REFRESH_TOKEN = auto() REFRESH_TOKEN = "REFRESH_TOKEN"
API_KEY = auto() API_KEY = "API_KEY"
class KeyStatus(str, Enum): class KeyStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
EXPIRED = auto() EXPIRED = "EXPIRED"
DELETED = auto() DELETED = "DELETED"
account_keyring_table = Table( account_keyring_table = Table(
@ -58,7 +60,7 @@ account_keyring_table = Table(
Column("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False), Column("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False),
Column("key_type", SQLAEnum(KeyType), primary_key=True, nullable=False), Column("key_type", SQLAEnum(KeyType), primary_key=True, nullable=False),
Column("key_id", String(40), primary_key=True, default=None), Column("key_id", String(40), primary_key=True, default=None),
Column("key_value", String(255), nullable=False), Column("key_value", String(512), nullable=False),
Column("created_at", DateTime(timezone=True), server_default=func.now()), Column("created_at", DateTime(timezone=True), server_default=func.now()),
Column("expiry", DateTime(timezone=True), nullable=True), Column("expiry", DateTime(timezone=True), nullable=True),
Column("status", SQLAEnum(KeyStatus), nullable=False), Column("status", SQLAEnum(KeyStatus), nullable=False),

View File

@ -1,3 +1,5 @@
import enum
from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index from sqlalchemy import Table, Column, Integer, String, Enum as SQLAEnum, JSON, ForeignKey, DateTime, Index
from sqlalchemy.sql import func from sqlalchemy.sql import func
from enum import Enum, auto from enum import Enum, auto
@ -7,15 +9,15 @@ from api.db.sql_types import UnsignedInt
from api.db import metadata from api.db import metadata
class EventState(str, Enum): class EventState(enum.StrEnum):
AUTO = auto() AUTO = "AUTO"
DESCRIPTED = auto() DESCRIPTED = "DESCRIPTED"
class EventStatus(str, Enum): class EventStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
DISABLED = auto() DISABLED = "DISABLED"
DELETED = auto() DELETED = "DELETED"
list_events_table = Table( list_events_table = Table(

View File

@ -1,3 +1,5 @@
import enum
from sqlalchemy import ( from sqlalchemy import (
Table, Table,
Column, Column,
@ -19,11 +21,11 @@ from api.db.sql_types import UnsignedInt
from api.db import metadata from api.db import metadata
class ProcessStatus(str, Enum): class ProcessStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
STOPPING = auto() STOPPING = "STOPPING"
STOPPED = auto() STOPPED = "STOPPED"
DELETED = auto() DELETED = "DELETED"
process_schema_table = Table( process_schema_table = Table(
@ -57,10 +59,10 @@ process_version_archive_table = Table(
) )
class NodeStatus(str, Enum): class NodeStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
DISABLED = auto() DISABLED = "DISABLED"
DELETED = auto() DELETED = "DELETED"
class NodeType(Enum): class NodeType(Enum):
@ -83,11 +85,11 @@ ps_node_table = Table(
) )
class NodeLinkStatus(str, Enum): class NodeLinkStatus(enum.StrEnum):
ACTIVE = auto() ACTIVE = "ACTIVE"
STOPPING = auto() STOPPING = "STOPPING"
STOPPED = auto() STOPPED = "STOPPED"
DELETED = auto() DELETED = "DELETED"
node_link_table = Table( node_link_table = Table(

View File

@ -1,28 +1,27 @@
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
Body,
Depends, Depends,
Form,
HTTPException, HTTPException,
Request,
Response,
status, status,
) )
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep from api.db.connection.session import get_connection_dep
from api.db.logic.account import (
from api.db.logic.account import get_user_id, update_user_id, create_user, get_user_login create_user,
get_user_accaunt_page,
from api.schemas.account.account import User, Status get_user_by_id,
from api.schemas.endpoints.account import UserUpdate get_user_by_login,
update_user_by_id,
)
from api.services.user_role_validation import db_user_role_validation from api.db.logic.keyring import create_password_key, update_password_key
from api.db.tables.account import AccountStatus
from api.schemas.account.account import User
from api.schemas.base import bearer_schema
from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate
from api.services.auth import get_current_user
from api.services.update_data_validation import update_user_data_changes from api.services.update_data_validation import update_user_data_changes
from api.services.user_role_validation import db_user_role_validation
api_router = APIRouter( api_router = APIRouter(
prefix="/account", prefix="/account",
@ -30,12 +29,32 @@ api_router = APIRouter(
) )
@api_router.get("/{user_id}") @api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllUserResponse)
async def get_account(user_id: int, request: Request, connection: AsyncConnection = Depends(get_connection_dep)): async def get_all_account(
current_user = request.state.current_user page: int = 1,
limit: int = 10,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id) user_list = await get_user_accaunt_page(connection, page, limit)
if user_list is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Accounts not found")
return user_list
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
async def get_account(
user_id: int,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_by_id(connection, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
@ -43,70 +62,77 @@ async def get_account(user_id: int, request: Request, connection: AsyncConnectio
return user return user
@api_router.post("") @api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser)
async def create_account(user: UserUpdate, request: Request, connection: AsyncConnection = Depends(get_connection_dep)): async def create_account(
current_user = request.state.current_user user: UserCreate,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
user_validation = await get_user_login(connection, user.login) user_validation = await get_user_by_login(connection, user.login)
if user_validation is None: if user_validation is None:
await create_user(connection, user, authorize_user.id) new_user = await create_user(connection, user, authorize_user.id)
user_new = await get_user_login(connection, user.login) await create_password_key(connection, user.password, new_user.id)
return user_new return new_user
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists." status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
) )
@api_router.put("/{user_id}") @api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
async def update_account( async def update_account(
user_id: int, request: Request, user_update: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep) user_id: int,
user_update: UserUpdate,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
): ):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id) user = await get_user_by_id(connection, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
if user_update.password is not None:
await update_password_key(connection, user.id, user_update.password)
update_values = update_user_data_changes(user_update, user) update_values = update_user_data_changes(user_update, user)
if update_values is None: if update_values is None:
return user return user
user_update_data = User.model_validate({**user.model_dump(), **update_values}) user_update_data = UserUpdate.model_validate({**user.model_dump(), **update_values})
await update_user_id(connection, update_values, user) await update_user_by_id(connection, update_values, user)
user = await get_user_id(connection, user_id) user = await get_user_by_id(connection, user_id)
return user return user
@api_router.delete("/{user_id}") @api_router.delete("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
async def delete_account(user_id: int, request: Request, connection: AsyncConnection = Depends(get_connection_dep)): async def delete_account(
current_user = request.state.current_user user_id: int,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
):
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
user = await get_user_id(connection, user_id) user = await get_user_by_id(connection, user_id)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
user_update = UserUpdate(status=Status.DELETED.value) user_update = UserUpdate(status=AccountStatus.DELETED.value)
update_values = update_user_data_changes(user_update, user) update_values = update_user_data_changes(user_update, user)
if update_values is None: if update_values is None:
return user return user
await update_user_id(connection, update_values, user) await update_user_by_id(connection, update_values, user)
user = await get_user_id(connection, user_id) user = await get_user_by_id(connection, user_id)
return user return user

View File

@ -2,13 +2,11 @@ from datetime import datetime, timedelta, timezone
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
Body,
Depends, Depends,
Form,
HTTPException, HTTPException,
Request,
Response, Response,
status, status,
Request,
) )
from loguru import logger from loguru import logger
@ -24,7 +22,7 @@ from api.services.auth import authenticate_user
from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token
from api.schemas.endpoints.auth import Auth, AccessToken from api.schemas.endpoints.auth import Auth, Tokens
api_router = APIRouter( api_router = APIRouter(
prefix="/auth", prefix="/auth",
@ -35,7 +33,7 @@ api_router = APIRouter(
class Settings(BaseModel): class Settings(BaseModel):
authjwt_secret_key: str = get_settings().SECRET_KEY authjwt_secret_key: str = get_settings().SECRET_KEY
# Configure application to store and get JWT from cookies # Configure application to store and get JWT from cookies
authjwt_token_location: set = {"headers", "cookies"} authjwt_token_location: set = {"headers"}
authjwt_cookie_domain: str = get_settings().DOMAIN authjwt_cookie_domain: str = get_settings().DOMAIN
# Only allow JWT cookies to be sent over https # Only allow JWT cookies to be sent over https
@ -50,7 +48,7 @@ def get_config():
return Settings() return Settings()
@api_router.post("") @api_router.post("", response_model=Tokens)
async def login_for_access_token( async def login_for_access_token(
user: Auth, user: Auth,
response: Response, response: Response,
@ -71,7 +69,6 @@ async def login_for_access_token(
) )
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS) refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS)
logger.debug(f"refresh_token_expires {refresh_token_expires}") logger.debug(f"refresh_token_expires {refresh_token_expires}")
@ -83,35 +80,27 @@ async def login_for_access_token(
await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user) await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user)
Authorize.set_refresh_cookies(refresh_token) return Tokens(access_token=access_token, refresh_token=refresh_token)
return AccessToken(access_token=access_token)
@api_router.post("/refresh") @api_router.post("/refresh", response_model=Tokens)
async def refresh( async def refresh(
request: Request, connection: AsyncConnection = Depends(get_connection_dep), Authorize: AuthJWT = Depends() request: Request,
): connection: AsyncConnection = Depends(get_connection_dep),
refresh_token = request.cookies.get("refresh_token_cookie") Authorize: AuthJWT = Depends(),
# print("Refresh Token:", refresh_token) ) -> Tokens:
if not refresh_token:
raise HTTPException(status_code=401, detail="Refresh token is missing")
try: try:
Authorize.jwt_refresh_token_required() Authorize.jwt_refresh_token_required()
current_user = Authorize.get_jwt_subject() current_user = Authorize.get_jwt_subject()
except Exception:
except Exception as e: refresh_token = request.headers.get("Authorization").split(" ")[1]
await upgrade_old_refresh_token(connection, current_user, refresh_token) await upgrade_old_refresh_token(connection, refresh_token)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token", detail="Invalid refresh token",
) )
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires) new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires)
return AccessToken(access_token=new_access_token) return Tokens(access_token=new_access_token)

View File

@ -4,7 +4,6 @@ from fastapi import (
Depends, Depends,
Form, Form,
HTTPException, HTTPException,
Request,
Response, Response,
status, status,
) )
@ -14,13 +13,15 @@ from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep from api.db.connection.session import get_connection_dep
from api.db.logic.keyring import get_key_id, create_key, update_key_id from api.db.logic.keyring import get_key_by_id, create_key, update_key_by_id
from api.schemas.account.account import Status from api.db.tables.account import KeyStatus
from api.schemas.base import bearer_schema
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
from api.schemas.account.account_keyring import AccountKeyring 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.user_role_validation import db_user_role_validation
from api.services.update_data_validation import update_key_data_changes from api.services.update_data_validation import update_key_data_changes
@ -32,13 +33,13 @@ api_router = APIRouter(
) )
@api_router.get("/{user_id}/{key_id}") @api_router.get("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
async def get_keyring(key_id: str, request: Request, connection: AsyncConnection = Depends(get_connection_dep)): async def get_keyring(
current_user = request.state.current_user key_id: str, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
):
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
if keyring is None: if keyring is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Key not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Key not found")
@ -46,27 +47,25 @@ async def get_keyring(key_id: str, request: Request, connection: AsyncConnection
return keyring return keyring
@api_router.post("/{user_id}/{key_id}") @api_router.post("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
async def create_keyring( async def create_keyring(
user_id: int, user_id: int,
key_id: str, key_id: str,
request: Request,
key: AccountKeyringUpdate, key: AccountKeyringUpdate,
connection: AsyncConnection = Depends(get_connection_dep), connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
): ):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
if keyring is None: if keyring is None:
user_new = await create_key( keyring_new = await create_key(
connection, connection,
key, key,
key_id, key_id,
) )
return user_new return keyring_new
else: else:
raise HTTPException( raise HTTPException(
@ -74,19 +73,17 @@ async def create_keyring(
) )
@api_router.put("/{user_id}/{key_id}") @api_router.put("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
async def update_keyring( async def update_keyring(
user_id: int, user_id: int,
key_id: str, key_id: str,
request: Request,
keyring_update: AccountKeyringUpdate, keyring_update: AccountKeyringUpdate,
connection: AsyncConnection = Depends(get_connection_dep), connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
): ):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
if keyring is None: if keyring is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
@ -97,34 +94,35 @@ async def update_keyring(
keyring_update_data = AccountKeyring.model_validate({**keyring.model_dump(), **update_values}) keyring_update_data = AccountKeyring.model_validate({**keyring.model_dump(), **update_values})
await update_key_id(connection, update_values, keyring) await update_key_by_id(connection, update_values, keyring)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
return keyring return keyring
@api_router.delete("/{user_id}/{key_id}") @api_router.delete("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
async def delete_keyring( async def delete_keyring(
user_id: int, key_id: str, request: Request, connection: AsyncConnection = Depends(get_connection_dep) user_id: int,
key_id: str,
connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
): ):
current_user = request.state.current_user
authorize_user = await db_user_role_validation(connection, current_user) authorize_user = await db_user_role_validation(connection, current_user)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
if keyring is None: if keyring is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
keyring_update = AccountKeyringUpdate(status=Status.DELETED.value) keyring_update = AccountKeyringUpdate(status=KeyStatus.DELETED.value)
update_values = update_key_data_changes(keyring_update, keyring) update_values = update_key_data_changes(keyring_update, keyring)
if update_values is None: if update_values is None:
return keyring return keyring
await update_key_id(connection, update_values, keyring) await update_key_by_id(connection, update_values, keyring)
keyring = await get_key_id(connection, key_id) keyring = await get_key_by_id(connection, key_id)
return keyring return keyring

View File

@ -13,10 +13,13 @@ from fastapi import (
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.connection.session import get_connection_dep from api.db.connection.session import get_connection_dep
from api.db.logic.account import get_user_id, update_user_id, get_user_login from api.db.logic.account import get_user_by_id, update_user_by_id, get_user_by_login
from api.schemas.base import bearer_schema
from api.services.auth import get_current_user
from api.services.update_data_validation import update_user_data_changes from api.services.update_data_validation import update_user_data_changes
from api.schemas.endpoints.account import UserUpdate from api.schemas.endpoints.account import UserUpdate
from api.schemas.account.account import User
api_router = APIRouter( api_router = APIRouter(
@ -25,15 +28,11 @@ api_router = APIRouter(
) )
@api_router.get("") @api_router.get("", dependencies=[Depends(bearer_schema)], response_model=User)
async def get_profile( async def get_profile(
request: Request, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
connection: AsyncConnection = Depends(get_connection_dep),
): ):
# Извлекаем текущего пользователя из request.state user = await get_user_by_login(connection, current_user)
current_user = request.state.current_user
user = await get_user_login(connection, current_user)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
@ -41,15 +40,13 @@ async def get_profile(
return user return user
@api_router.put("") @api_router.put("", dependencies=[Depends(bearer_schema)], response_model=User)
async def update_profile( async def update_profile(
request: Request,
user_updata: UserUpdate, user_updata: UserUpdate,
connection: AsyncConnection = Depends(get_connection_dep), connection: AsyncConnection = Depends(get_connection_dep),
current_user=Depends(get_current_user),
): ):
current_user = request.state.current_user user = await get_user_by_login(connection, current_user)
user = await get_user_login(connection, current_user)
if user is None: if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
@ -59,9 +56,9 @@ async def update_profile(
if update_values is None: if update_values is None:
return user return user
await update_user_id(connection, update_values, user) await update_user_by_id(connection, update_values, user)
user = await get_user_id(connection, user.id) user = await get_user_by_id(connection, user.id)
return user return user
else: else:

View File

@ -1,37 +1,20 @@
import datetime import datetime
from enum import Enum
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from pydantic import EmailStr, Field from pydantic import EmailStr, Field
from api.db.tables.account import AccountRole, AccountStatus
from api.schemas.base import Base from api.schemas.base import Base
# Модель для хранения информации из запроса
class Role(Enum):
OWNER = "OWNER"
ADMIN = "ADMIN"
EDITOR = "EDITOR"
VIEWER = "VIEWER"
class Status(Enum):
ACTIVE = "ACTIVE"
DISABLED = "DISABLED"
BLOCKED = "BLOCKED"
DELETED = "DELETED"
class User(Base): class User(Base):
id: Optional[int] = None id: Optional[int] = None
name: str = Field(..., max_length=100) name: str = Field(..., max_length=100)
login: str = Field(..., max_length=100) login: str = Field(..., max_length=100)
email: Optional[EmailStr] = Field(None, max_length=100) # Электронная почта (может быть None) email: Optional[EmailStr] = Field(None, max_length=100) # Электронная почта (может быть None)
bind_tenant_id: Optional[str] = Field(None, max_length=40) bind_tenant_id: Optional[str] = Field(None, max_length=40)
role: Role role: AccountRole
meta: dict meta: dict
creator_id: Optional[int] = None creator_id: Optional[int] = None
created_at: datetime created_at: datetime
status: Status status: AccountStatus

View File

@ -1,33 +1,17 @@
import datetime import datetime
from enum import Enum from typing import Optional
from typing import Optional, Dict
from pydantic import Field from pydantic import Field
from datetime import datetime from datetime import datetime
from api.db.tables.account import KeyType, KeyStatus
from api.schemas.base import Base from api.schemas.base import Base
# Модель для хранения информации из запроса
class TypeKey(Enum):
PASSWORD = "PASSWORD"
ACCESS_TOKEN = "ACCESS_TOKEN"
REFRESH_TOKEN = "REFRESH_TOKEN"
API_KEY = "API_KEY"
class StatusKey(Enum):
ACTIVE = "ACTIVE"
EXPIRED = "EXPIRED"
DELETED = "DELETED"
class AccountKeyring(Base): class AccountKeyring(Base):
owner_id: int owner_id: int
key_type: TypeKey # Используем тот же KeyType key_type: KeyType
key_id: Optional[str] = Field(None, max_length=40) # Изменено на None как default key_id: Optional[str] = Field(None, max_length=40)
key_value: str = Field(..., max_length=255) key_value: str = Field(..., max_length=255)
created_at: datetime created_at: datetime
expiry: Optional[datetime] = None expiry: Optional[datetime] = None
status: StatusKey status: KeyStatus

View File

@ -1,7 +1,11 @@
from fastapi.security import HTTPBearer
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
bearer_schema = HTTPBearer() # схема для авторизации в swagger
class Base(BaseModel): class Base(BaseModel):
model_config = ConfigDict( model_config = ConfigDict(
from_attributes=True, from_attributes=True,

View File

@ -1,36 +1,54 @@
from enum import Enum
from typing import Optional
from datetime import datetime from datetime import datetime
from pydantic import EmailStr, Field from typing import List, Optional
from pydantic import EmailStr, Field, TypeAdapter
from api.db.tables.account import AccountRole, AccountStatus
from api.schemas.base import Base from api.schemas.base import Base
# Таблица для получения информации из запроса
class Role(Enum):
OWNER = "OWNER"
ADMIN = "ADMIN"
EDITOR = "EDITOR"
VIEWER = "VIEWER"
class Status(Enum):
ACTIVE = "ACTIVE"
DISABLED = "DISABLED"
BLOCKED = "BLOCKED"
DELETED = "DELETED"
class UserUpdate(Base): class UserUpdate(Base):
id: Optional[int] = None id: Optional[int] = None
name: Optional[str] = Field(None, max_length=100) name: Optional[str] = Field(None, max_length=100)
login: Optional[str] = Field(None, max_length=100) login: Optional[str] = Field(None, max_length=100)
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
password: Optional[str] = None
bind_tenant_id: Optional[str] = Field(None, max_length=40) bind_tenant_id: Optional[str] = Field(None, max_length=40)
role: Optional[Role] = None role: Optional[AccountRole] = None
meta: Optional[dict] = None meta: Optional[dict] = None
creator_id: Optional[int] = None creator_id: Optional[int] = None
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
status: Optional[Status] = 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
login: str
email: Optional[EmailStr] = None
bind_tenant_id: Optional[str] = None
role: AccountRole
created_at: datetime
status: AccountStatus
class AllUserResponse(Base):
users: List[AllUser]
amount_count: int
amount_pages: int
current_page: int
limit: int
all_user_adapter = TypeAdapter(List[AllUser])

View File

@ -1,33 +1,17 @@
import datetime import datetime
from enum import Enum
from typing import Optional from typing import Optional
from pydantic import Field from pydantic import Field
from datetime import datetime from datetime import datetime
from api.db.tables.account import KeyType, KeyStatus
from api.schemas.base import Base from api.schemas.base import Base
# Таблица для получения информации из запроса
class TypeKey(Enum):
PASSWORD = "PASSWORD"
ACCESS_TOKEN = "ACCESS_TOKEN"
REFRESH_TOKEN = "REFRESH_TOKEN"
API_KEY = "API_KEY"
class StatusKey(Enum):
ACTIVE = "ACTIVE"
EXPIRED = "EXPIRED"
DELETED = "DELETED"
class AccountKeyringUpdate(Base): class AccountKeyringUpdate(Base):
owner_id: Optional[int] = None owner_id: Optional[int] = None
key_type: Optional[TypeKey] = None key_type: Optional[KeyType] = None
key_id: Optional[str] = Field(None, max_length=40) key_id: Optional[str] = Field(None, max_length=40)
key_value: Optional[str] = Field(None, max_length=255) key_value: Optional[str] = Field(None, max_length=255)
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
expiry: Optional[datetime] = None expiry: Optional[datetime] = None
status: Optional[StatusKey] = None status: Optional[KeyStatus] = None

View File

@ -1,6 +1,5 @@
from api.schemas.base import Base from api.schemas.base import Base
# Таблица для получения информации из запроса # Таблица для получения информации из запроса
@ -9,9 +8,6 @@ class Auth(Base):
password: str password: str
class AccessToken(Base): class Tokens(Base):
access_token: str access_token: str
refresh_token: str | None = None
class Refresh(Base):
refresh_token: str

View File

@ -1,4 +1,4 @@
from pydantic import Field, conint from pydantic import Field
from typing import Dict, Any from typing import Dict, Any
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum

View File

@ -1,18 +1,25 @@
from typing import Optional from typing import Optional
from fastapi import HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.ext.asyncio import AsyncConnection
from api.db.logic.auth import get_user from api.db.logic.auth import get_user
from api.db.tables.account import AccountStatus
# # from backend.schemas.users.token import TokenData from api.schemas.endpoints.account import AllUser
from api.schemas.account.account import User, Status from api.utils.hasher import hasher
from api.utils.hasher import Hasher
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> 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[AllUser]:
sql_user, sql_password = await get_user(connection, username) sql_user, sql_password = await get_user(connection, username)
if not sql_user or sql_user.status != Status.ACTIVE: if not sql_user or sql_user.status != AccountStatus.ACTIVE:
return None return None
hasher = Hasher()
if not hasher.verify_data(password, sql_password.key_value): if not hasher.verify_data(password, sql_password.key_value):
return None return None
return sql_user return sql_user

View File

@ -1,3 +1,4 @@
from fastapi_jwt_auth import AuthJWT
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import ( from fastapi import (
Request, Request,
@ -11,9 +12,6 @@ import re
from re import escape from re import escape
from fastapi_jwt_auth import AuthJWT
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware): class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
def __init__(self, app): def __init__(self, app):
super().__init__(app) super().__init__(app)
@ -22,13 +20,18 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
self.excluded_routes = [ self.excluded_routes = [
re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"), re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"),
re.compile(r"^" + re.escape(self.prefix) + r"/auth/?$"), re.compile(r"^" + re.escape(self.prefix) + r"/auth/?$"),
re.compile(r"^" + r"/swagger"),
re.compile(r"^" + r"/openapi"),
] ]
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
if request.method in ["GET", "POST", "PUT", "DELETE"]: if request.method not in ["GET", "POST", "PUT", "DELETE"]:
return JSONResponse(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
content={"detail": "Method not allowed"},
)
if any(pattern.match(request.url.path) for pattern in self.excluded_routes): if any(pattern.match(request.url.path) for pattern in self.excluded_routes):
return await call_next(request) return await call_next(request)
else:
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if not auth_header: if not auth_header:
return JSONResponse( return JSONResponse(
@ -36,26 +39,15 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
content={"detail": "Missing authorization header."}, content={"detail": "Missing authorization header."},
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try:
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
Authorize = AuthJWT(request) Authorize = AuthJWT(request)
try:
current_user = Authorize.get_jwt_subject() current_user = Authorize.get_jwt_subject()
request.state.current_user = current_user request.state.current_user = current_user
return await call_next(request)
except Exception: except Exception:
return JSONResponse( return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "The access token is invalid or expired."}, content={"detail": "The access token is invalid or expired."},
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return await call_next(request)
# async with get_connection() as connection:
# authorize_user = await get_user_login(connection, current_user)
# print(authorize_user)
# if authorize_user is None :
# return JSONResponse(
# status_code=status.HTTP_404_NOT_FOUND ,
# detail="User not found.")

View File

@ -1,7 +1,9 @@
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from api.schemas.endpoints.account import UserUpdate, Role, Status from api.schemas.endpoints.account import UserUpdate
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate, StatusKey, TypeKey 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]: def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]:
@ -18,7 +20,7 @@ def update_user_data_changes(update_data: UserUpdate, user) -> Optional[dict]:
if value is None: if value is None:
continue continue
if isinstance(value, (Role, Status)): if isinstance(value, (AccountRole, AccountStatus)):
update_values[field] = value.value update_values[field] = value.value
else: else:
update_values[field] = value update_values[field] = value
@ -52,7 +54,7 @@ def update_key_data_changes(update_data: AccountKeyringUpdate, key) -> Optional[
if value is None: if value is None:
continue continue
if isinstance(value, (TypeKey, StatusKey)): if isinstance(value, (KeyType, KeyStatus)):
update_values[field] = value.value update_values[field] = value.value
else: else:
update_values[field] = value update_values[field] = value

View File

@ -2,12 +2,12 @@ from fastapi import (
HTTPException, HTTPException,
status, status,
) )
from api.db.logic.account import get_user_login from api.db.logic.account import get_user_by_login
from api.schemas.account.account import Role, Status from api.db.tables.account import AccountRole
async def db_user_role_validation(connection, current_user): async def db_user_role_validation(connection, current_user):
authorize_user = await get_user_login(connection, current_user) authorize_user = await get_user_by_login(connection, current_user)
if authorize_user.role not in {Role.OWNER, Role.ADMIN}: if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions")
return authorize_user return authorize_user

View File

@ -1,4 +1,6 @@
import hashlib import hashlib
import secrets
# Хешер для работы с паролем. # Хешер для работы с паролем.
@ -14,3 +16,10 @@ class Hasher:
def verify_data(self, password: str, hashed: str) -> bool: def verify_data(self, password: str, hashed: str) -> bool:
# Проверяет пароль путем сравнения его хеша с сохраненным хешем. # Проверяет пароль путем сравнения его хеша с сохраненным хешем.
return self.hash_data(password) == hashed return self.hash_data(password) == hashed
@staticmethod
def generate_password() -> str:
return secrets.token_urlsafe(20)
hasher = Hasher()

View File

@ -1,32 +1,23 @@
import os
import asyncio import asyncio
import hashlib import os
import secrets
from api.db.connection.session import get_connection from api.db.connection.session import get_connection
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus from api.db.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
from api.utils.hasher import hasher
from api.utils.key_id_gen import KeyIdGenerator from api.utils.key_id_gen import KeyIdGenerator
INIT_LOCK_FILE = "../init.lock" INIT_LOCK_FILE = "../init.lock"
DEFAULT_LOGIN = "vorkout" DEFAULT_LOGIN = "vorkout"
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
def generate_password() -> str:
return secrets.token_urlsafe(20)
async def init(): async def init():
if os.path.exists(INIT_LOCK_FILE): if os.path.exists(INIT_LOCK_FILE):
print("Sorry, service is already initialized") print("Sorry, service is already initialized")
return return
async with get_connection() as conn: async with get_connection() as conn:
password = generate_password() password = hasher.generate_password()
hashed_password = hash_password(password) hashed_password = hasher.hash_data(password)
create_user_query = account_table.insert().values( create_user_query = account_table.insert().values(
name=DEFAULT_LOGIN, name=DEFAULT_LOGIN,

565
api/poetry.lock generated
View File

@ -106,6 +106,18 @@ doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]]
name = "certifi"
version = "2025.4.26"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"},
{file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"},
]
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "1.17.1" version = "1.17.1"
@ -338,14 +350,40 @@ files = [
] ]
[package.dependencies] [package.dependencies]
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""}
fastapi-cli = {version = ">=0.0.5", extras = ["standard"], optional = true, markers = "extra == \"standard\""}
httpx = {version = ">=0.23.0", optional = true, markers = "extra == \"standard\""}
jinja2 = {version = ">=3.1.5", optional = true, markers = "extra == \"standard\""}
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
python-multipart = {version = ">=0.0.18", optional = true, markers = "extra == \"standard\""}
starlette = ">=0.40.0,<0.47.0" starlette = ">=0.40.0,<0.47.0"
typing-extensions = ">=4.8.0" typing-extensions = ">=4.8.0"
uvicorn = {version = ">=0.12.0", extras = ["standard"], optional = true, markers = "extra == \"standard\""}
[package.extras] [package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastapi-cli"
version = "0.0.7"
description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4"},
{file = "fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e"},
]
[package.dependencies]
rich-toolkit = ">=0.11.1"
typer = ">=0.12.3"
uvicorn = {version = ">=0.15.0", extras = ["standard"]}
[package.extras]
standard = ["uvicorn[standard] (>=0.15.0)"]
[[package]] [[package]]
name = "fastapi-jwt-auth" name = "fastapi-jwt-auth"
version = "0.5.0" version = "0.5.0"
@ -472,6 +510,109 @@ files = [
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
] ]
[[package]]
name = "httpcore"
version = "1.0.8"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"},
{file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httptools"
version = "0.6.4"
description = "A collection of framework independent HTTP protocol utils."
optional = false
python-versions = ">=3.8.0"
groups = ["main"]
files = [
{file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
{file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
{file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"},
{file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"},
{file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"},
{file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"},
{file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"},
{file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"},
{file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"},
{file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"},
{file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"},
{file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"},
{file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"},
{file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"},
{file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"},
{file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"},
{file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"},
{file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"},
{file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"},
{file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"},
{file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"},
{file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"},
{file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"},
{file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"},
{file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"},
{file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"},
{file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"},
{file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"},
{file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"},
{file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"},
{file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"},
{file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"},
{file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"},
{file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"},
{file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"},
{file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"},
{file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"},
{file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"},
{file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"},
{file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"},
{file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"},
{file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"},
{file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"},
]
[package.extras]
test = ["Cython (>=0.29.24)"]
[[package]]
name = "httpx"
version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@ -487,6 +628,24 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "jinja2"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "loguru" name = "loguru"
version = "0.7.3" version = "0.7.3"
@ -526,6 +685,31 @@ babel = ["Babel"]
lingua = ["lingua"] lingua = ["lingua"]
testing = ["pytest"] testing = ["pytest"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "3.0.2" version = "3.0.2"
@ -597,6 +781,18 @@ files = [
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
] ]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.2.0" version = "6.2.0"
@ -992,6 +1188,21 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
toml = ["tomli (>=2.0.1)"] toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"] yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pygments"
version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "1.7.1" version = "1.7.1"
@ -1052,6 +1263,105 @@ files = [
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
] ]
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "rich"
version = "14.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
groups = ["main"]
files = [
{file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"},
{file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "rich-toolkit"
version = "0.14.7"
description = "Rich toolkit for building command-line applications"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "rich_toolkit-0.14.7-py3-none-any.whl", hash = "sha256:def05cc6e0f1176d6263b6a26648f16a62c4563b277ca2f8538683acdba1e0da"},
{file = "rich_toolkit-0.14.7.tar.gz", hash = "sha256:6cca5a68850cc5778915f528eb785662c27ba3b4b2624612cce8340fa9701c5e"},
]
[package.dependencies]
click = ">=8.1.7"
rich = ">=13.7.1"
typing-extensions = ">=4.12.2"
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.11.10" version = "0.11.10"
@ -1080,6 +1390,18 @@ files = [
{file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"}, {file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"},
] ]
[[package]]
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"
@ -1208,6 +1530,24 @@ anyio = ">=3.6.2,<5"
[package.extras] [package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
[[package]]
name = "typer"
version = "0.16.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855"},
{file = "typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b"},
]
[package.dependencies]
click = ">=8.0.0"
rich = ">=10.11.0"
shellingham = ">=1.3.0"
typing-extensions = ">=3.7.4.3"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -1249,11 +1589,234 @@ files = [
[package.dependencies] [package.dependencies]
click = ">=7.0" click = ">=7.0"
colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""}
h11 = ">=0.8" h11 = ">=0.8"
httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""}
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
[package.extras] [package.extras]
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "uvloop"
version = "0.21.0"
description = "Fast implementation of asyncio event loop on top of libuv"
optional = false
python-versions = ">=3.8.0"
groups = ["main"]
markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
files = [
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
{file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"},
{file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"},
{file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"},
{file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"},
{file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"},
{file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"},
{file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"},
{file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"},
{file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"},
{file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"},
{file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"},
{file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"},
{file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"},
{file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"},
{file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"},
{file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"},
{file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"},
{file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"},
{file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"},
{file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"},
{file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"},
{file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"},
{file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"},
{file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"},
{file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"},
{file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"},
{file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"},
{file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"},
{file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"},
{file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"},
{file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"},
{file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"},
{file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"},
{file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"},
{file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"},
]
[package.extras]
dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
[[package]]
name = "watchfiles"
version = "1.0.5"
description = "Simple, modern and high performance file watching and code reload in python."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"},
{file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"},
{file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"},
{file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"},
{file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"},
{file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"},
{file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"},
{file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"},
{file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"},
{file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"},
{file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"},
{file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"},
{file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"},
{file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"},
{file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"},
{file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"},
{file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"},
{file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"},
{file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"},
{file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"},
{file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"},
{file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"},
{file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"},
{file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"},
{file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"},
{file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"},
{file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"},
{file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"},
{file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"},
{file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"},
{file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"},
{file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"},
{file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"},
{file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"},
{file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"},
{file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"},
{file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"},
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"},
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"},
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"},
{file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"},
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"},
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"},
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"},
{file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"},
{file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"},
]
[package.dependencies]
anyio = ">=3.0.0"
[[package]]
name = "websockets"
version = "15.0.1"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
{file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"},
{file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"},
{file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"},
{file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"},
{file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"},
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"},
{file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"},
{file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"},
{file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"},
{file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"},
{file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"},
{file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"},
{file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"},
{file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"},
{file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"},
{file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"},
{file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"},
{file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"},
{file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"},
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"},
{file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"},
{file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"},
{file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"},
{file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"},
{file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"},
{file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"},
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"},
{file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"},
{file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"},
{file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"},
{file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"},
{file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"},
{file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"},
{file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"},
{file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"},
{file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"},
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
]
[[package]] [[package]]
name = "win32-setctime" name = "win32-setctime"
version = "1.2.0" version = "1.2.0"
@ -1370,4 +1933,4 @@ propcache = ">=0.2.0"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11,<4.0" python-versions = ">=3.11,<4.0"
content-hash = "22129fa3f5a2cc1190af6c7645f3dd8b4ab0f873b120987cd2e06772863e7dc8" content-hash = "5ed129fde2c5d7b3518fbb2fe2ce79f0bad2aa1060304d45a7bc26d35f7ab46b"

View File

@ -1,17 +1,15 @@
[project] [project]
name = "api" name = "api"
version = "0.0.2" version = "0.0.5"
description = "" description = ""
authors = [ authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }]
{name = "Vladislav",email = "vlad.dev@heado.ru"}
]
readme = "README.md" readme = "README.md"
requires-python = ">=3.11,<4.0" requires-python = ">=3.11,<4.0"
dependencies = [ dependencies = [
"sqlalchemy[pymysql,aiomysql] (>=2.0.39,<3.0.0)", "sqlalchemy[pymysql,aiomysql] (>=2.0.39,<3.0.0)",
"alembic (>=1.15.1,<2.0.0)", "alembic (>=1.15.1,<2.0.0)",
"aio-pika (>=9.5.5,<10.0.0)", "aio-pika (>=9.5.5,<10.0.0)",
"fastapi[standart] (>=0.115.11,<0.116.0)", "fastapi[standard] (>=0.115.11,<0.116.0)",
"uvicorn (>=0.34.0,<0.35.0)", "uvicorn (>=0.34.0,<0.35.0)",
"loguru (>=0.7.3,<0.8.0)", "loguru (>=0.7.3,<0.8.0)",
"pydantic-settings (>=2.8.1,<3.0.0)", "pydantic-settings (>=2.8.1,<3.0.0)",

View File

@ -1,5 +1,4 @@
REACT_APP_WEBSOCKET_PROTOCOL=ws VITE_APP_WEBSOCKET_PROTOCOL=ws
REACT_APP_HTTP_PROTOCOL=http VITE_APP_HTTP_PROTOCOL=http
REACT_APP_API_URL=localhost:8000 VITE_APP_API_URL=localhost:8000
REACT_APP_URL=localhost:3000 VITE_APP_URL=localhost:3000
BROWSER=none

17
client/index.html Normal file
View 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>

17469
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "client", "name": "client",
"version": "0.0.2", "version": "0.0.5",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
@ -9,25 +9,24 @@
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.0.11", "@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"antd": "^5.24.7", "antd": "^5.24.7",
"axios": "^1.9.0",
"axios-retry": "^4.5.0",
"i18next": "^25.0.1", "i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5", "i18next-browser-languagedetector": "^8.0.5",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.5.1", "react-i18next": "^15.5.1",
"react-router-dom": "^7.5.0", "react-router-dom": "^7.5.0",
"react-scripts": "5.0.1", "web-vitals": "^2.1.4",
"typescript": "^4.9.5", "zustand": "^5.0.5"
"web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "dev": "vite",
"build": "react-scripts build", "build": "vite build",
"test": "react-scripts test", "preview": "vite preview"
"eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -46,5 +45,13 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@types/node": "^20.19.1",
"@vitejs/plugin-react": "^4.5.2",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-node-polyfills": "^0.23.0"
} }
} }

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,3 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.0626 73.9965L52.0348 76.7513C51.9791 79.687 49.6 82.0452 46.6644 82.0452C43.8957 82.0452 41.6139 79.9444 41.3357 77.2383L41.3217 77.1965C41.3217 76.953 41.1269 76.7583 40.8835 76.7513C40.8835 76.7513 36.7235 76.1391 35.5548 75.8052C34.7757 75.5826 34.7061 73.9965 35.9722 73.9965H52.0696H52.0626ZM76.0904 73.9965L76.1183 76.7513C76.1739 79.687 78.5531 82.0452 81.4887 82.0452C84.2574 82.0452 86.5391 79.9444 86.8174 77.2383L86.8313 77.1965C86.8313 76.953 87.0261 76.7583 87.2696 76.7513C87.2696 76.7513 91.4296 76.1391 92.5983 75.8052C93.3774 75.5826 93.447 73.9965 92.1809 73.9965H76.0835H76.0904ZM58.233 127.979V103.123C54.6643 105.468 51.0957 105.085 50.4557 105.023V126.804C53.0157 127.353 55.6104 127.75 58.233 127.979ZM69.8157 103.123V128C72.4383 127.777 75.0331 127.388 77.593 126.845V105.023C76.953 105.085 73.3774 105.468 69.8157 103.123ZM127.993 64.1183C127.993 109.913 86.88 124.139 85.3704 124.668V96.2157H79.5339C72.6957 96.2157 67.1583 90.5809 67.1583 83.6313V73.9965H60.9948V83.6313C60.9948 90.5809 55.4574 96.2157 48.6191 96.2157H42.6783V124.591C26.6435 118.908 0 99.9861 0 64.1183C0 24.9252 32.4244 0 64 0C103.847 0 128 32.487 128 64.1183H127.993ZM56.5774 70.5948C56.5774 65.5443 52.5565 61.4539 47.5896 61.4539H35.4504C30.4835 61.4539 26.4626 65.5443 26.4626 70.5948V81.5652C26.4626 86.6226 30.4835 90.7061 35.4504 90.7061H47.5896C52.5565 90.7061 56.5774 86.6157 56.5774 81.5652V70.5948ZM79.5339 55.9444C79.5339 55.9444 87.8609 55.9652 93.1687 55.9861V43.4713C93.1687 33.6765 85.2452 25.7391 75.4644 25.7391H52.6539C42.88 25.7391 34.9496 33.6765 34.9496 43.4713V55.9861C40.2504 55.9722 48.6122 55.9444 48.6122 55.9444C55.2417 55.9444 60.633 61.2383 60.96 67.8957H67.1861C67.5131 61.2383 72.9044 55.9444 79.5339 55.9444ZM92.7026 90.6991C97.6696 90.6991 101.69 86.6087 101.69 81.5583V70.5878C101.69 65.5374 97.6696 61.447 92.7026 61.447H80.5635C75.5965 61.447 71.5757 65.5374 71.5757 70.5878V81.5583C71.5757 86.6087 75.5965 90.6991 80.5635 90.6991H92.7026ZM120.188 64.1113C120.188 33.0713 95.7009 7.81914 64 7.81914C32.2991 7.81914 7.81218 33.0713 7.81218 64.1113C7.81218 84.487 18.6783 102.372 34.9078 112.257V96.2087H31.9165C25.0852 96.2087 19.5409 90.5739 19.5409 83.6244V74.087H18.3165C17.5931 74.087 17.0017 73.5374 17.0017 72.8626V69.1478C17.0017 68.473 17.5931 67.9235 18.3165 67.9235H19.5617C19.7565 62.8035 22.8522 58.5461 27.2765 56.8V41.2313C27.2765 28.8348 37.3078 18.7896 49.6765 18.7896H78.4696C90.8383 18.7896 100.87 28.8348 100.87 41.2313V56.793C105.301 58.5322 108.41 62.7965 108.605 67.9235H109.85C110.574 67.9235 111.165 68.473 111.165 69.1478V72.8626C111.165 73.5374 110.574 74.087 109.85 74.087H108.626V83.6244C108.626 90.5739 103.089 96.2087 96.2505 96.2087H93.1548V112.383C109.503 102.525 120.188 84.5774 120.188 64.1113Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -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>

View File

@ -1,13 +1,25 @@
import React from 'react'; /* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect } from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import MainLayout from './pages/MainLayout'; import { useSetUserSelector } from './store/userStore';
import LoginPage from './pages/LoginPage';
import ProtectedRoute from './pages/ProtectedRoute'; import ProtectedRoute from './pages/ProtectedRoute';
import MainLayout from './pages/MainLayout';
function App() { function App() {
const setUser = useSetUserSelector();
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
setUser(JSON.parse(storedUser));
}
}, []);
return ( return (
<div className="App"> <div className="App">
<Routes> <Routes>
<Route path="/login" element={<div>login</div>} /> <Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
<Route path="*" element={<MainLayout />}></Route> <Route path="*" element={<MainLayout />}></Route>
</Route> </Route>

130
client/src/api/api.ts Normal file
View File

@ -0,0 +1,130 @@
import axios from 'axios';
import axiosRetry from 'axios-retry';
import { Auth, Tokens } from '@/types/auth';
import { useAuthStore } from '@/store/authStore';
import { AuthService } from '@/services/authService';
import { User, UserCreate, UserUpdate } from '@/types/user';
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
import.meta.env.VITE_APP_API_URL
}/api/v1`;
const base = axios.create({
baseURL,
withCredentials: true,
headers: {
accepts: 'application/json',
},
});
base.interceptors.request.use((config) => {
if (config.url === '/auth/refresh') {
return config;
}
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axiosRetry(base, {
retries: 3,
retryDelay: (retryCount: number) => {
console.log(`retry attempt: ${retryCount}`);
return retryCount * 2000;
},
retryCondition: async (error: any) => {
if (error.code === 'ERR_CANCELED') {
return true;
}
return false;
},
});
base.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
if (!error.response) {
return Promise.reject(error);
}
console.log('error', error);
const originalRequest = error.response.config;
const urlTokens = error?.request?.responseURL.split('/');
const url = urlTokens[urlTokens.length - 1];
console.log('url', url);
if (
error.response.status === 401 &&
!(originalRequest?._retry != null) &&
url !== 'login' &&
url !== 'refresh' &&
url !== 'logout'
) {
originalRequest._retry = true;
try {
await AuthService.refresh();
return base(originalRequest);
} catch (error) {
await AuthService.logout();
return new Promise(() => {});
}
}
return await Promise.reject(error);
}
);
const api = {
// auth
async login(auth: Auth): Promise<Tokens> {
const response = await base.post<Tokens>('/auth', auth);
return response.data;
},
async refreshToken(): Promise<Tokens> {
const token = localStorage.getItem('refreshToken');
const response = await base.post<Tokens>(
'/auth/refresh',
{},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
},
// user
async getProfile(): Promise<User> {
const response = await base.get<User>('/profile');
return response.data;
},
async getUsers(page: number, limit: number): Promise<any> {
const response = await base.get<User[]>(
`/account?page=${page}&limit=${limit}`
);
return response.data;
},
async getUserById(userId: number): Promise<User> {
const response = await base.get<User>(`/account/${userId}`);
return response.data;
},
async createUser(user: UserCreate): Promise<User> {
const response = await base.post<User>('/account', user);
return response.data;
},
async updateUser(userId: number, user: UserUpdate): Promise<User> {
const response = await base.put<User>(`/account/${userId}`, user);
return response.data;
},
// keyrings
};
export default api;

View File

@ -2,12 +2,16 @@ import { Drawer } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Avatar, Typography } from 'antd'; import { Avatar, Typography } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUserSelector } from '@/store/userStore';
interface ContentDrawerProps { interface ContentDrawerProps {
open: boolean; open: boolean;
closeDrawer: () => void; closeDrawer: () => void;
children: React.ReactNode; children: React.ReactNode;
type: 'create' | 'edit'; type: 'create' | 'edit';
login?: string;
name?: string;
email?: string | null;
} }
export default function ContentDrawer({ export default function ContentDrawer({
@ -15,7 +19,11 @@ export default function ContentDrawer({
closeDrawer, closeDrawer,
children, children,
type, type,
login,
name,
email,
}: ContentDrawerProps) { }: ContentDrawerProps) {
const user = useUserSelector();
const { t } = useTranslation(); const { t } = useTranslation();
const [width, setWidth] = useState<number | string>('30%'); const [width, setWidth] = useState<number | string>('30%');
@ -30,6 +38,7 @@ export default function ContentDrawer({
window.addEventListener('resize', calculateWidths); window.addEventListener('resize', calculateWidths);
return () => window.removeEventListener('resize', calculateWidths); return () => window.removeEventListener('resize', calculateWidths);
}, []); }, []);
console.log(login, user?.login, login === user?.login);
const editDrawerTitle = ( const editDrawerTitle = (
<div <div
@ -59,16 +68,21 @@ export default function ContentDrawer({
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
<Avatar <Avatar
src="https://cdn-icons-png.flaticon.com/512/219/219986.png" src={
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
}
size={40} size={40}
style={{ flexShrink: 0 }} style={{ flexShrink: 0 }}
/> />
<div> <div>
<Typography.Text strong style={{ display: 'block' }}> <Typography.Text
Александр Александров strong
style={{ display: 'block', fontSize: '20px' }}
>
{name} {login === user?.login ? t('you') : ''}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 14 }}> <Typography.Text type="secondary" style={{ fontSize: 14 }}>
alexandralex@vorkout.ru {email}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>
@ -152,7 +166,7 @@ export default function ContentDrawer({
placement="right" placement="right"
open={open} open={open}
width={width} width={width}
destroyOnClose={true} destroyOnHidden={true}
closable={false} closable={false}
> >
{children} {children}

View File

@ -1,5 +1,9 @@
import { useUserSelector } from '@/store/userStore';
import { Avatar } from 'antd'; import { Avatar } from 'antd';
import Title from 'antd/es/typography/Title'; import Title from 'antd/es/typography/Title';
import { useState } from 'react';
import ContentDrawer from './ContentDrawer';
import UserEdit from './UserEdit';
interface HeaderProps { interface HeaderProps {
title: string; title: string;
@ -7,6 +11,13 @@ interface HeaderProps {
} }
export default function Header({ title, additionalContent }: HeaderProps) { export default function Header({ title, additionalContent }: HeaderProps) {
const [openEdit, setOpenEdit] = useState(false);
const showEditDrawer = () => setOpenEdit(true);
const closeEditDrawer = () => {
setOpenEdit(false);
};
const user = useUserSelector();
return ( return (
<div <div
style={{ style={{
@ -43,13 +54,24 @@ export default function Header({ title, additionalContent }: HeaderProps) {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
onClick={showEditDrawer}
> >
<Avatar <Avatar
size={25.77} size={25.77}
src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`} src={`https://gamma.heado.ru/go/ava?name=${user?.login}`}
/> />
</div> </div>
</div> </div>
<ContentDrawer
login={user?.login}
name={user?.name}
email={user?.email}
open={openEdit}
closeDrawer={closeEditDrawer}
type="edit"
>
{user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
</ContentDrawer>
</div> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import { useUserSelector } from '@/store/userStore';
import { Divider, Menu, Tooltip } from 'antd'; import { Divider, Menu, Tooltip } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -13,6 +14,7 @@ export default function SiderMenu({
selectedKey, selectedKey,
hangleMenuClick, hangleMenuClick,
}: SiderMenuProps) { }: SiderMenuProps) {
const user = useUserSelector();
const { t } = useTranslation(); const { t } = useTranslation();
const collapseStyle = collapsed const collapseStyle = collapsed
? { fontSize: '12px' } ? { fontSize: '12px' }
@ -74,7 +76,8 @@ export default function SiderMenu({
label: t('settings'), label: t('settings'),
className: 'no-expand-icon', className: 'no-expand-icon',
children: [ children: [
{ user && (user.role === 'OWNER' || user.role === 'ADMIN')
? {
key: '/accounts', key: '/accounts',
label: !collapsed ? ( label: !collapsed ? (
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip> <Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
@ -82,7 +85,8 @@ export default function SiderMenu({
t('accounts') t('accounts')
), ),
style: collapseStyle, style: collapseStyle,
}, }
: undefined,
{ {
key: '/events-list', key: '/events-list',
label: !collapsed ? ( label: !collapsed ? (

View File

@ -8,13 +8,19 @@ import {
UploadFile, UploadFile,
GetProp, GetProp,
UploadProps, UploadProps,
} from 'antd'; message,
import { useState } from 'react'; Spin,
import { useTranslation } from 'react-i18next'; } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useUserSelector } from "@/store/userStore";
import { UserCreate as NewUserCreate } from "@/types/user";
import { UserService } from "@/services/userService";
import { LoadingOutlined } from "@ant-design/icons";
const { Option } = Select; const { Option } = Select;
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0]; type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
const getBase64 = (file: FileType): Promise<string> => const getBase64 = (file: FileType): Promise<string> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -24,10 +30,17 @@ const getBase64 = (file: FileType): Promise<string> =>
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
export default function UserCreate() { interface UserCreateProps {
closeDrawer: () => void;
getUsers: () => Promise<void>;
}
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
const user = useUserSelector();
const { t } = useTranslation(); const { t } = useTranslation();
const [previewOpen, setPreviewOpen] = useState(false); const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState(''); const [previewImage, setPreviewImage] = useState("");
const [loading, setLoading] = useState(false);
const [fileList, setFileList] = useState<UploadFile[]>([]); const [fileList, setFileList] = useState<UploadFile[]>([]);
@ -40,40 +53,49 @@ export default function UserCreate() {
setPreviewOpen(true); setPreviewOpen(true);
}; };
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
setFileList(newFileList); setFileList(newFileList);
const onFinish = async (values: NewUserCreate) => {
setLoading(true);
await UserService.createUser(values);
await getUsers();
closeDrawer();
setLoading(false);
message.info(t("createdAccountMessage"), 4);
};
const customUploadButton = ( const customUploadButton = (
<div> <div>
<div <div
style={{ style={{
height: '102px', height: "102px",
width: '102px', width: "102px",
backgroundColor: '#E2E2E2', backgroundColor: "#E2E2E2",
borderRadius: '50%', borderRadius: "50%",
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
marginBottom: 8, marginBottom: 8,
marginTop: 30, marginTop: 30,
cursor: 'pointer', cursor: "pointer",
}} }}
> >
<img <img
src="./icons/drawer/add_photo_alternate.svg" src="./icons/drawer/add_photo_alternate.svg"
alt="add_photo_alternate" alt="add_photo_alternate"
style={{ height: '18px', width: '18px' }} style={{ height: "18px", width: "18px" }}
/> />
</div> </div>
<span style={{ fontSize: '14px', color: '#8c8c8c' }}> <span style={{ fontSize: "14px", color: "#8c8c8c" }}>
{t('selectPhoto')} {t("selectPhoto")}
</span> </span>
</div> </div>
); );
const photoToUpload = ( const photoToUpload = (
<div style={{ height: '102px' }}> <div style={{ height: "102px" }}>
<Upload <Upload
listType="picture-circle" listType="picture-circle"
fileList={fileList} fileList={fileList}
@ -85,11 +107,11 @@ export default function UserCreate() {
</Upload> </Upload>
{previewImage && ( {previewImage && (
<Image <Image
wrapperStyle={{ display: 'none' }} wrapperStyle={{ display: "none" }}
preview={{ preview={{
visible: previewOpen, visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible), onVisibleChange: (visible) => setPreviewOpen(visible),
afterOpenChange: (visible) => !visible && setPreviewImage(''), afterOpenChange: (visible) => !visible && setPreviewImage(""),
}} }}
src={previewImage} src={previewImage}
/> />
@ -100,24 +122,24 @@ export default function UserCreate() {
return ( return (
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
height: '100%', height: "100%",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
marginBottom: '36px', marginBottom: "36px",
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
}} }}
> >
{photoToUpload} {photoToUpload}
@ -126,83 +148,85 @@ export default function UserCreate() {
<Form <Form
name="user-edit-form" name="user-edit-form"
layout="vertical" layout="vertical"
// onFinish={onFinish} onFinish={onFinish}
initialValues={{ initialValues={{
name: '', name: "",
login: '', login: "",
password: '', password: "",
email: '', email: "",
tenant: '', bindTenantId: "",
role: '', role: "",
status: '', status: "",
}} }}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }} style={{ flex: 1, display: "flex", flexDirection: "column" }}
> >
<Form.Item <Form.Item
label={t('name')} label={t("name")}
name="name" name="name"
rules={[{ required: true, message: t('nameMessage') }]} rules={[{ required: true, message: t("nameMessage") }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('login')} label={t("login")}
name="login" name="login"
rules={[{ required: true, message: t('loginMessage') }]} rules={[{ required: true, message: t("loginMessage") }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('password')} label={t("password")}
name="password" name="password"
rules={[{ required: true, message: t('passwordMessage') }]} rules={[{ message: t("passwordMessage") }]}
> >
<Input.Password /> <Input.Password />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('email')} label={t("email")}
name="email" name="email"
rules={[ rules={[
{ required: true, message: t('emailMessage') }, { message: t("emailMessage") },
{ type: 'email', message: t('emailErrorMessage') }, { type: "email", message: t("emailErrorMessage") },
]} ]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('tenant')} label={t("tenant")}
name="tenant" name="bindTenantId"
rules={[{ required: true, message: t('tenantMessage') }]} rules={[{ message: t("tenantMessage") }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('role')} label={t("role")}
name="role" name="role"
rules={[{ required: true, message: t('roleMessage') }]} rules={[{ required: true, message: t("roleMessage") }]}
> >
<Select placeholder={t('roleMessage')}> <Select placeholder={t("roleMessage")}>
<Option value="Директор магазина">Директор магазина</Option> {user && user.role === "OWNER" ? (
<Option value="Менеджер">Менеджер</Option> <Option value="ADMIN">{t("ADMIN")}</Option>
<Option value="Кассир">Кассир</Option> ) : undefined}
<Option value="EDITOR">{t("EDITOR")}</Option>
<Option value="VIEWER">{t("VIEWER")}</Option>
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('status')} label={t("status")}
name="status" name="status"
rules={[{ required: true, message: t('statusMessage') }]} rules={[{ required: true, message: t("statusMessage") }]}
> >
<Select placeholder={t('statusMessage')}> <Select placeholder={t("statusMessage")}>
<Option value="ACTIVE">Активен</Option> <Option value="ACTIVE">{t("ACTIVE")}</Option>
<Option value="DISABLED">Неактивен</Option> <Option value="DISABLED">{t("DISABLED")}</Option>
<Option value="BLOCKED">Заблокирован</Option> <Option value="BLOCKED">{t("BLOCKED")}</Option>
<Option value="DELETED">Удален</Option> <Option value="DELETED">{t("DELETED")}</Option>
</Select> </Select>
</Form.Item> </Form.Item>
@ -213,14 +237,23 @@ export default function UserCreate() {
type="primary" type="primary"
htmlType="submit" htmlType="submit"
block block
style={{ color: '#000' }} style={{ color: "#000" }}
> >
{loading ? (
<>
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "}
{t("saving")}
</>
) : (
<>
<img <img
src="/icons/drawer/reg.svg" src="/icons/drawer/reg.svg"
alt="save" alt="save"
style={{ height: '18px', width: '18px' }} style={{ height: "18px", width: "18px" }}
/>{' '} />{" "}
{t('addAccount')} {t("addAccount")}
</>
)}
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -1,47 +1,101 @@
import { Button, Form, Input, Select } from 'antd'; import { UserService } from '@/services/userService';
import { useUserSelector } from '@/store/userStore';
import { UserUpdate } from '@/types/user';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Form, Input, message, Select, Spin } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const { Option } = Select; const { Option } = Select;
export default function UserEdit() { interface UserEditProps {
userId?: number;
closeDrawer: () => void;
}
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
const currentUser = useUserSelector();
const [form] = Form.useForm();
const { t } = useTranslation(); const { t } = useTranslation();
const [user, setUser] = useState<UserUpdate>({
id: 0,
name: '',
login: '',
email: '',
password: '',
bindTenantId: '',
role: 'VIEWER',
meta: {},
createdAt: '',
status: 'ACTIVE',
});
const [loading, setLoading] = useState(false);
useEffect(() => {
async function getUser() {
if (typeof userId === 'undefined') {
return;
}
const user = await UserService.getUserById(userId);
setUser(user);
form.setFieldsValue({ ...user });
}
getUser();
}, []);
const onFinish = async (values: UserUpdate) => {
setLoading(true);
let updatedUser: Partial<UserUpdate> = {};
(Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
if (values[key] !== user[key]) {
updatedUser = { ...updatedUser, [key]: values[key] };
}
});
if (Object.keys(updatedUser).length > 0) {
console.log('updateUser', userId, updatedUser);
await UserService.updateUser(userId!, updatedUser);
}
setLoading(false);
message.info(t('editAccountMessage'), 4);
closeDrawer();
};
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<Form <Form
form={form}
name="user-edit-form" name="user-edit-form"
layout="vertical" layout="vertical"
// onFinish={onFinish} onFinish={onFinish}
initialValues={{ initialValues={{ ...user }}
name: 'Александр Александров',
login: 'alexandralex@vorkout.ru',
password: 'jKUUl776GHd',
email: 'alexandralex@vorkout.ru',
tenant: 'text',
role: 'Директор магазина',
status: 'Активен',
}}
style={{ flex: 1, display: 'flex', flexDirection: 'column' }} style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
> >
<Form.Item <Form.Item
label={t('name')} label={t('name')}
name="name" name="name"
rules={[{ required: true, message: t('nameMessage') }]} rules={[{ message: t('nameMessage') }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
{user?.id === currentUser?.id ? undefined : (
<Form.Item <Form.Item
label={t('login')} label={t('login')}
name="login" name="login"
rules={[{ required: true, message: t('loginMessage') }]} rules={[{ message: t('loginMessage') }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
)}
<Form.Item <Form.Item
label={t('password')} label={t('password')}
name="password" name="password"
rules={[{ required: true, message: t('passwordMessage') }]} rules={[{ message: t('passwordMessage') }]}
> >
<Input.Password /> <Input.Password />
</Form.Item> </Form.Item>
@ -59,23 +113,27 @@ export default function UserEdit() {
<Form.Item <Form.Item
label={t('tenant')} label={t('tenant')}
name="tenant" name="bindTenantId"
rules={[{ required: true, message: t('tenantMessage') }]} rules={[{ required: true, message: t('tenantMessage') }]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
{user?.id === currentUser?.id ? undefined : (
<Form.Item <Form.Item
label={t('role')} label={t('role')}
name="role" name="role"
rules={[{ required: true, message: t('roleMessage') }]} rules={[{ required: true, message: t('roleMessage') }]}
> >
<Select placeholder={t('roleMessage')}> <Select placeholder={t('roleMessage')}>
<Option value="Директор магазина">Директор магазина</Option> {currentUser && currentUser.role === 'OWNER' ? (
<Option value="Менеджер">Менеджер</Option> <Option value="ADMIN">{t('ADMIN')}</Option>
<Option value="Кассир">Кассир</Option> ) : undefined}
<Option value="EDITOR">{t('EDITOR')}</Option>
<Option value="VIEWER">{t('VIEWER')}</Option>
</Select> </Select>
</Form.Item> </Form.Item>
)}
<Form.Item <Form.Item
label={t('status')} label={t('status')}
@ -83,10 +141,10 @@ export default function UserEdit() {
rules={[{ required: true, message: t('statusMessage') }]} rules={[{ required: true, message: t('statusMessage') }]}
> >
<Select placeholder={t('statusMessage')}> <Select placeholder={t('statusMessage')}>
<Option value="ACTIVE">Активен</Option> <Option value="ACTIVE">{t('ACTIVE')}</Option>
<Option value="DISABLED">Неактивен</Option> <Option value="DISABLED">{t('DISABLED')}</Option>
<Option value="BLOCKED">Заблокирован</Option> <Option value="BLOCKED">{t('BLOCKED')}</Option>
<Option value="DELETED">Удален</Option> <Option value="DELETED">{t('DELETED')}</Option>
</Select> </Select>
</Form.Item> </Form.Item>
@ -99,12 +157,21 @@ export default function UserEdit() {
block block
style={{ color: '#000' }} style={{ color: '#000' }}
> >
{loading ? (
<>
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '}
{t('saving')}
</>
) : (
<>
<img <img
src="/icons/drawer/save.svg" src="/icons/drawer/save.svg"
alt="save" alt="save"
style={{ height: '18px', width: '18px' }} style={{ height: '18px', width: '18px' }}
/>{' '} />{' '}
{t('save')} {t('save')}
</>
)}
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@ -1,8 +1,9 @@
import './i18n'; import '@/config/i18n';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BrowserRouter } from 'react-router-dom';
import { theme } from './customTheme'; import { theme } from '@/config/customTheme';
import en from 'antd/locale/en_US'; import en from 'antd/locale/en_US';
import ru from 'antd/locale/ru_RU'; import ru from 'antd/locale/ru_RU';
@ -18,7 +19,7 @@ export default function AppWrapper({ children }: any) {
return ( return (
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}> <ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
{children} <BrowserRouter>{children}</BrowserRouter>
</ConfigProvider> </ConfigProvider>
); );
} }

View File

@ -37,6 +37,20 @@ i18n
addAccount: 'Add account', addAccount: 'Add account',
save: 'Save changes', save: 'Save changes',
newAccount: 'New account', newAccount: 'New account',
ACTIVE: 'Active',
DISABLED: 'Disabled',
BLOCKED: 'Blocked',
DELETED: 'Deleted',
OWNER: 'Owner',
ADMIN: 'Admin',
EDITOR: 'Editor',
VIEWER: 'Viewer',
nameLogin: 'Name, login',
createdAt: 'Created',
saving: 'Saving...',
createdAccountMessage: 'User successfully created!',
editAccountMessage: 'User successfully updated!',
you: '(You)',
}, },
}, },
ru: { ru: {
@ -66,6 +80,20 @@ i18n
addAccount: 'Добавить аккаунт', addAccount: 'Добавить аккаунт',
save: 'Сохранить изменения', save: 'Сохранить изменения',
newAccount: 'Новая учетная запись', newAccount: 'Новая учетная запись',
ACTIVE: 'Активен',
DISABLED: 'Выключен',
BLOCKED: 'Заблокирован',
DELETED: 'Удален',
OWNER: 'Владелец',
ADMIN: 'Админ',
EDITOR: 'Редактор',
VIEWER: 'Наблюдатель',
nameLogin: 'Имя, Логин',
createdAt: 'Создано',
saving: 'Сохранение...',
createdAccountMessage: 'Пользователь успешно создан!',
editAccountMessage: 'Пользователь успешно обновлен!',
you: '(Вы)',
}, },
}, },
}, },

12
client/src/env.d.ts vendored Normal file
View 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;
}

View File

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import '@/index.css';
import App from './App'; import App from '@/App';
import { BrowserRouter } from 'react-router-dom'; import AppWrapper from '@/config/AppWrapper';
import AppWrapper from './config/AppWrapper';
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
@ -11,8 +10,6 @@ const root = ReactDOM.createRoot(
root.render( root.render(
<AppWrapper> <AppWrapper>
<BrowserRouter>
<App /> <App />
</BrowserRouter>
</AppWrapper> </AppWrapper>
); );

View File

@ -1,36 +1,219 @@
import Header from '../components/Header'; import { useEffect, useState } from "react";
import { useState } from 'react'; import { useTranslation } from "react-i18next";
import ContentDrawer from '../components/ContentDrawer'; import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
import UserCreate from '../components/UserCreate'; import Header from "@/components/Header";
import { useTranslation } from 'react-i18next'; import ContentDrawer from "@/components/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() { export default function AccountsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [openCreate, setOpenCreate] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const showDrawer = () => setOpen(true); const [activeAccount, setActiveAccount] = useState<
const closeDrawer = () => setOpen(false); { login: string; id: number; name: string; email: string } | undefined
>(undefined);
const showCreateDrawer = () => setOpenCreate(true);
const closeCreateDrawer = () => {
setActiveAccount(undefined);
setOpenCreate(false);
};
const [openEdit, setOpenEdit] = useState(false);
const showEditDrawer = () => setOpenEdit(true);
const closeEditDrawer = () => {
setActiveAccount(undefined);
setOpenEdit(false);
};
const [accounts, setAccounts] = useState<AllUserResponse>({
amountCount: 0,
amountPages: 0,
users: [],
currentPage: 1,
limit: 10,
});
async function getUsers() {
const page = Number(searchParams.get("page") || "1");
const limit = Number(searchParams.get("limit") || "10");
setSearchParams({
page: page.toString(),
limit: limit.toString(),
});
const data = await UserService.getUsers(page, limit);
console.log("searchParams", searchParams);
setAccounts(data);
}
useEffect(() => {
getUsers();
}, []);
const statusColor = {
ACTIVE: "#27AE60",
DISABLED: "#606060",
BLOCKED: "#FF0000",
DELETED: "#B30000",
};
const columns: TableProps<AllUser>["columns"] = [
{
title: "#",
dataIndex: "id",
key: "id",
},
{
title: t("nameLogin"),
dataIndex: "nameLogin",
key: "nameLogin",
render: (text, record) => (
<div
onClick={() => {
setActiveAccount({
login: record.login,
id: record.id,
name: record.name,
email: record.email || "",
});
showEditDrawer();
}}
style={{
display: "flex",
alignItems: "center",
gap: "16px",
cursor: "pointer",
}}
>
<div
style={{
height: "32px",
width: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Avatar
size={32}
src={`https://gamma.heado.ru/go/ava?name=${record.login}`}
/>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<div>{record.name}</div>
<div style={{ color: "#606060" }}>{record.login}</div>
</div>
</div>
),
},
{
title: "E-mail",
dataIndex: "email",
key: "email",
},
{
title: t("tenant"),
dataIndex: "bindTenantId",
key: "tenant",
},
{
title: t("role"),
dataIndex: "role",
key: "role",
render: (text) => <div>{t(text)}</div>,
},
{
title: t("createdAt"),
dataIndex: "createdAt",
key: "createdAt",
render: (text) => (
<div>
{new Date(text).toLocaleString("ru", {
year: "2-digit",
month: "2-digit",
day: "2-digit",
})}
</div>
),
},
{
title: t("status"),
dataIndex: "status",
key: "status",
render: (text) => (
<div style={{ color: statusColor[text as AccountStatus] }}>
{t(text)}
</div>
),
},
];
const onTableChange: TableProps<AllUser>["onChange"] = (pagination) => {
console.log(pagination);
UserService.getUsers(
pagination.current as number,
pagination.pageSize
).then((data) => {
setAccounts(data);
setSearchParams({
page: data.currentPage.toString(),
limit: data.limit.toString(),
});
});
};
return ( return (
<> <>
<Header <Header
title={t('accounts')} title={t("accounts")}
additionalContent={ additionalContent={
<img <img
src="./icons/header/add_2.svg" src="./icons/header/add_2.svg"
alt="add" alt="add"
style={{ style={{
height: '18px', height: "18px",
width: '18px', width: "18px",
cursor: 'pointer', cursor: "pointer",
}} }}
onClick={showDrawer} onClick={showCreateDrawer}
/> />
} }
/> />
<Table
size="small"
onChange={onTableChange}
columns={columns}
dataSource={accounts.users}
pagination={{
pageSize: accounts.limit,
current: accounts.currentPage,
total: accounts.amountCount,
}}
rowKey={"id"}
/>
<ContentDrawer open={open} closeDrawer={closeDrawer} type="create"> <ContentDrawer
<UserCreate /> open={openCreate}
closeDrawer={closeCreateDrawer}
type="create"
>
<UserCreate getUsers={getUsers} closeDrawer={closeCreateDrawer} />
</ContentDrawer>
<ContentDrawer
login={activeAccount?.login}
name={activeAccount?.name}
email={activeAccount?.email}
open={openEdit}
closeDrawer={closeEditDrawer}
type="edit"
>
<UserEdit userId={activeAccount?.id} closeDrawer={closeEditDrawer} />
</ContentDrawer> </ContentDrawer>
</> </>
); );

View File

@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ConfigurationPage() { export default function ConfigurationPage() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function EventsListPage() { export default function EventsListPage() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -0,0 +1,107 @@
import React from 'react';
import { Form, Input, Button, Typography, message } from 'antd';
import {
EyeInvisibleOutlined,
EyeTwoTone,
UserOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { AuthService } from '@/services/authService';
import { Auth } from '@/types/auth';
const { Text, Link } = Typography;
export default function LoginPage() {
const navigate = useNavigate();
const onFinish = async (values: any) => {
await AuthService.login(values as Auth);
navigate('/');
};
return (
<div
style={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div
style={{
width: '100%',
maxWidth: 472,
padding: 24,
background: '#fff',
textAlign: 'center',
}}
>
<div style={{ marginBottom: 32 }}>
<img
src="./icons/logo.svg"
alt="logo"
style={{ width: 128, height: 128, marginBottom: 16 }}
/>
</div>
<Form
name="login"
onFinish={onFinish}
layout="vertical"
>
<Form.Item
name="login"
rules={[{ required: true, message: 'Введите login' }]}
>
<Input size="large" placeholder="Логин" prefix={<UserOutlined />} />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Введите пароль' }]}
>
<Input.Password
size="large"
placeholder="Пароль"
iconRender={(visible) =>
visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />
}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
block
size="large"
style={{
backgroundColor: '#C2DA3D',
borderColor: '#C2DA3D',
color: '#000',
}}
>
Войти
</Button>
</Form.Item>
<Text type="secondary" style={{ fontSize: 12 }}>
Нажимая кнопку Войти, Вы полностью принимаете{' '}
<Link href="/offer" target="_blank">
Публичную оферту
</Link>{' '}
и{' '}
<Link href="/privacy" target="_blank">
Политику обработки персональных данных
</Link>
</Text>
</Form>
<div style={{ marginTop: 256 }}>
<Link href="/forgot-password">Забыли пароль?</Link>
</div>
</div>
</div>
);
}

View File

@ -2,8 +2,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import Sider from 'antd/es/layout/Sider'; import Sider from 'antd/es/layout/Sider';
import SiderMenu from '../components/SiderMenu';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import SiderMenu from '@/components/SiderMenu';
import ProcessDiagramPage from './ProcessDiagramPage'; import ProcessDiagramPage from './ProcessDiagramPage';
import RunningProcessesPage from './RunningProcessesPage'; import RunningProcessesPage from './RunningProcessesPage';
import AccountsPage from './AccountsPage'; import AccountsPage from './AccountsPage';

View File

@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function ProcessDiagramPage() { export default function ProcessDiagramPage() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,8 +1,18 @@
// ProtectedRoute.js /* eslint-disable react-hooks/exhaustive-deps */
import { Outlet } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import React from 'react'; import React, { useEffect } from 'react';
import { useUserSelector } from '@/store/userStore';
const ProtectedRoute = (): React.JSX.Element => { const ProtectedRoute = (): React.JSX.Element => {
const navigate = useNavigate();
const user = useUserSelector();
useEffect(() => {
if (!user?.id) {
navigate('/login');
}
}, [user]);
return <Outlet />; return <Outlet />;
}; };
export default ProtectedRoute; export default ProtectedRoute;

View File

@ -1,5 +1,5 @@
import Header from '@/components/Header';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Header from '../components/Header';
export default function RunningProcessesPage() { export default function RunningProcessesPage() {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -0,0 +1,30 @@
import api from "@/api/api";
import { useAuthStore } from "@/store/authStore";
import { Auth } from "@/types/auth";
import { UserService } from "./userService";
import { useUserStore } from "@/store/userStore";
export class AuthService {
static async login(auth: Auth) {
const token = await api.login(auth);
useAuthStore.getState().setAccessToken(token.accessToken);
localStorage.setItem('refreshToken', token.refreshToken as string);
await UserService.getProfile().then((user) => {
useUserStore.getState().setUser(user);
});
}
static async logout() {
console.log('logout');
useUserStore.getState().setUser(null);
useAuthStore.getState().setAccessToken(null);
localStorage.removeItem('userInfo');
localStorage.removeItem('refreshToken');
}
static async refresh() {
console.log('refresh');
const token = await api.refreshToken();
useAuthStore.getState().setAccessToken(token.accessToken);
}
}

View File

@ -0,0 +1,38 @@
import api from '@/api/api';
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
export class UserService {
static async getProfile(): Promise<User> {
console.log('getProfile');
const user = api.getProfile();
return user;
}
static async getUsers(
page: number = 1,
limit: number = 10
): Promise<AllUserResponse> {
console.log('getUsers');
const allUsers = api.getUsers(page, limit);
return allUsers;
}
static async getUserById(userId: number): Promise<User> {
console.log('getUserById');
const user = api.getUserById(userId);
return user;
}
static async createUser(user: UserCreate): Promise<User> {
console.log('createUser');
const createdUser = api.createUser(user);
return createdUser;
}
static async updateUser(userId: number, user: UserUpdate): Promise<User> {
console.log('updateUser');
const updatedUser = api.updateUser(userId, user);
return updatedUser;
}
}

View File

@ -0,0 +1,18 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
type AuthState = {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
};
export const useAuthStore = create<AuthState>()(
devtools((set) => ({
accessToken: null,
setAccessToken: (token) => set({ accessToken: token }),
}))
);
export const useAuthSelector = () => {
return useAuthStore((state) => state.accessToken);
};

View File

@ -0,0 +1,36 @@
import { User } from '@/types/user';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const userInfo = localStorage.getItem('userInfo');
type UserStoreState = {
user: User | null;
loading: boolean;
};
type UserStoreActions = {
setUser: (user: User | null) => void;
};
type UserStore = UserStoreState & UserStoreActions;
export const useUserStore = create<UserStore>()(
devtools(
persist(
(set, get) => ({
user: userInfo != null ? JSON.parse(userInfo) : ({} as User),
loading: false,
setUser: (user: User | null) => set({ user }),
}),
{ name: 'userInfo' }
)
)
);
export const useUserSelector = () => {
return useUserStore((state) => state.user);
};
export const useSetUserSelector = () => {
return useUserStore((state) => state.setUser);
};

4
client/src/types/auth.ts Normal file
View File

@ -0,0 +1,4 @@
import { components } from './openapi-types';
export type Auth = components['schemas']['Auth'];
export type Tokens = components['schemas']['Tokens'];

View File

@ -0,0 +1,719 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/api/v1/auth": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Login For Access Token
* @description Авторизирует, выставляет токены в куки.
*/
post: operations["login_for_access_token_api_v1_auth_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/auth/refresh": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Refresh */
post: operations["refresh_api_v1_auth_refresh_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/profile": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Profile */
get: operations["get_profile_api_v1_profile_get"];
/** Update Profile */
put: operations["update_profile_api_v1_profile_put"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/account": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get All Account */
get: operations["get_all_account_api_v1_account_get"];
put?: never;
/** Create Account */
post: operations["create_account_api_v1_account_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/account/{user_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Account */
get: operations["get_account_api_v1_account__user_id__get"];
/** Update Account */
put: operations["update_account_api_v1_account__user_id__put"];
post?: never;
/** Delete Account */
delete: operations["delete_account_api_v1_account__user_id__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/keyring/{user_id}/{key_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get Keyring */
get: operations["get_keyring_api_v1_keyring__user_id___key_id__get"];
/** Update Keyring */
put: operations["update_keyring_api_v1_keyring__user_id___key_id__put"];
/** Create Keyring */
post: operations["create_keyring_api_v1_keyring__user_id___key_id__post"];
/** Delete Keyring */
delete: operations["delete_keyring_api_v1_keyring__user_id___key_id__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** AccountKeyring */
AccountKeyring: {
/** Ownerid */
ownerId: number;
keyType: components["schemas"]["KeyType"];
/** Keyid */
keyId?: string | null;
/** Keyvalue */
keyValue: string;
/**
* Createdat
* Format: date-time
*/
createdAt: string;
/** Expiry */
expiry?: string | null;
status: components["schemas"]["KeyStatus"];
};
/** AccountKeyringUpdate */
AccountKeyringUpdate: {
/** Ownerid */
ownerId?: number | null;
keyType?: components["schemas"]["KeyType"] | null;
/** Keyid */
keyId?: string | null;
/** Keyvalue */
keyValue?: string | null;
/** Createdat */
createdAt?: string | null;
/** Expiry */
expiry?: string | null;
status?: components["schemas"]["KeyStatus"] | null;
};
/**
* AccountRole
* @enum {string}
*/
AccountRole: "OWNER" | "ADMIN" | "EDITOR" | "VIEWER";
/**
* AccountStatus
* @enum {string}
*/
AccountStatus: "ACTIVE" | "DISABLED" | "BLOCKED" | "DELETED";
/** AllUser */
AllUser: {
/** Id */
id: number;
/** Name */
name: string;
/** Login */
login: string;
/** Email */
email?: string | null;
/** Bindtenantid */
bindTenantId?: string | null;
role: components["schemas"]["AccountRole"];
/**
* Createdat
* Format: date-time
*/
createdAt: string;
status: components["schemas"]["AccountStatus"];
};
/** AllUserResponse */
AllUserResponse: {
/** Users */
users: components["schemas"]["AllUser"][];
/** Amountcount */
amountCount: number;
/** Amountpages */
amountPages: number;
/** Currentpage */
currentPage: number;
/** Limit */
limit: number;
};
/** Auth */
Auth: {
/** Login */
login: string;
/** Password */
password: string;
};
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
detail?: components["schemas"]["ValidationError"][];
};
/**
* KeyStatus
* @enum {string}
*/
KeyStatus: "ACTIVE" | "EXPIRED" | "DELETED";
/**
* KeyType
* @enum {string}
*/
KeyType: "PASSWORD" | "ACCESS_TOKEN" | "REFRESH_TOKEN" | "API_KEY";
/** Tokens */
Tokens: {
/** Accesstoken */
accessToken: string;
/** Refreshtoken */
refreshToken?: string | null;
};
/** User */
User: {
/** Id */
id?: number | null;
/** Name */
name: string;
/** Login */
login: string;
/** Email */
email?: string | null;
/** Bindtenantid */
bindTenantId?: string | null;
role: components["schemas"]["AccountRole"];
/** Meta */
meta: {
[key: string]: unknown;
};
/** Creatorid */
creatorId?: number | null;
/**
* Createdat
* Format: date-time
*/
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 */
id?: number | null;
/** 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;
/** Creatorid */
creatorId?: number | null;
/** Createdat */
createdAt?: string | null;
status?: components["schemas"]["AccountStatus"] | null;
};
/** ValidationError */
ValidationError: {
/** Location */
loc: (string | number)[];
/** Message */
msg: string;
/** Error Type */
type: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
login_for_access_token_api_v1_auth_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["Auth"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Tokens"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
refresh_api_v1_auth_refresh_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Tokens"];
};
};
};
};
get_profile_api_v1_profile_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
};
};
update_profile_api_v1_profile_put: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_all_account_api_v1_account_get: {
parameters: {
query?: {
page?: number;
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AllUserResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
create_account_api_v1_account_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserCreate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AllUser"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_account_api_v1_account__user_id__get: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_account_api_v1_account__user_id__put: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_account_api_v1_account__user_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_keyring_api_v1_keyring__user_id___key_id__get: {
parameters: {
query?: never;
header?: never;
path: {
key_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AccountKeyring"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_keyring_api_v1_keyring__user_id___key_id__put: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
key_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AccountKeyringUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AccountKeyring"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
create_keyring_api_v1_keyring__user_id___key_id__post: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
key_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AccountKeyringUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AccountKeyring"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_keyring_api_v1_keyring__user_id___key_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
key_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AccountKeyring"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}

9
client/src/types/user.ts Normal file
View File

@ -0,0 +1,9 @@
import { components } from './openapi-types';
export type User = components['schemas']['User'];
export type AllUserResponse = components['schemas']['AllUserResponse'];
export type AllUser = components['schemas']['AllUser'];
export type AccountStatus = components['schemas']['AccountStatus'];
export type AccountRole = components['schemas']['AccountRole'];
export type UserUpdate = components['schemas']['UserUpdate'];
export type UserCreate = components['schemas']['UserCreate'];

View File

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -18,9 +14,11 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx",
}, "baseUrl": ".",
"include": [ "paths": {
"src" "@/*": ["./src/*"]
] }
},
"include": ["src"]
} }

23
client/vite.config.ts Normal file
View 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'),
},
},
});

View File

@ -32,11 +32,11 @@ startretries=5
[program:client] [program:client]
environment= environment=
REACT_APP_WEBSOCKET_PROTOCOL=ws, VITE_APP_WEBSOCKET_PROTOCOL=ws,
REACT_APP_HTTP_PROTOCOL=http, VITE_APP_HTTP_PROTOCOL=http,
REACT_APP_API_URL=localhost:8000, VITE_APP_API_URL=localhost:8000,
REACT_APP_URL=localhost:3000 VITE_APP_URL=localhost:3000
command=bash -c 'cd client; npm run build; serve -s build' command=bash -c 'cd client; npm run build; npm run preview'
numprocs=1 numprocs=1
process_name=node-%(process_num)d process_name=node-%(process_num)d
stdout_logfile=client.out.log stdout_logfile=client.out.log