Compare commits
140 Commits
VORKOUT-2
...
VORKOUT-19
Author | SHA1 | Date | |
---|---|---|---|
7fd0a732b3 | |||
571186151b | |||
1b95c9825b | |||
cf6e09d8c1 | |||
|
d9e88d66f7 | ||
|
3c300f7c7a | ||
|
c66c21eb14 | ||
|
a9ecaadad6 | ||
|
e50bb32470 | ||
|
2030d54b2c | ||
|
b82960faf3 | ||
|
e0887c240f | ||
5e3c3b4672 | |||
60160d46be | |||
d03600b23d | |||
82fcd22faf | |||
|
2690843954 | ||
2493cd0d9f | |||
e6589a0950 | |||
a0fcc324fa | |||
2a35386c1d | |||
|
f65f4caf5c | ||
|
a90a802659 | ||
|
3d5d717962 | ||
|
0bff556b10 | ||
a936500101 | |||
4c0beb24f9 | |||
ad1369c3e3 | |||
|
f550b86c68 | ||
5958f29ba8 | |||
784be40369 | |||
ad312d4ff8 | |||
ba65f36696 | |||
ad0a4837fc | |||
1eadd834e3 | |||
8f3fde623f | |||
edfd2c1159 | |||
3d8ee4835d | |||
9c9201f130 | |||
0eed0b0f20 | |||
7127d88524 | |||
692461e266 | |||
22064d2b52 | |||
febac9659f | |||
6c0a6ac1d4 | |||
a7e813b447 | |||
919758ab69 | |||
53bf173373 | |||
8f5dd07bf5 | |||
448e4264a5 | |||
e5dfdc3464 | |||
aae56a8c73 | |||
7c2c4071cc | |||
18bb79262c | |||
a3ee18f6fd | |||
71ab39a03c | |||
5d39065a7f | |||
92ff1d3d0a | |||
5ed8ca9251 | |||
8ac329e76e | |||
c68b512902 | |||
70aaaeabf1 | |||
92203351ff | |||
ee92428ec3 | |||
c87581c9e2 | |||
79cb434ebd | |||
5def1a9bb1 | |||
d55a99aafd | |||
599bf22bda | |||
e114f963ab | |||
d7a5109d8e | |||
8122f1878a | |||
9d2aef5671 | |||
34be97996e | |||
e0dca78ef3 | |||
0bba6e7f54 | |||
21216a6ad5 | |||
2bf0f20e73 | |||
abd87b46b3 | |||
787dc0e8f8 | |||
2e4e9d1113 | |||
c1d315d6e9 | |||
|
a31758192d | ||
8965365afc | |||
|
c68286f7cc | ||
|
58cc23f79b | ||
|
48d52bf903 | ||
|
66c60cfc59 | ||
|
c60d19262e | ||
|
31236d558f | ||
|
5094b84675 | ||
7e1fe6f5c4 | |||
|
320c13183f | ||
|
98a4692247 | ||
8613cbdaac | |||
5f981e8ce1 | |||
98e425862c | |||
|
96dbc744d7 | ||
|
e47e449a36 | ||
86d48d0d1c | |||
|
c3c421f66f | ||
fe91bb7103 | |||
f97d419467 | |||
131102beba | |||
881a72a66c | |||
de06890f6a | |||
8191ee3a48 | |||
|
eb63ebb10f | ||
|
3554d43d15 | ||
|
19f8236b47 | ||
f1214e7b5a | |||
dbe3e3ab86 | |||
53729813ff | |||
dc1b74348f | |||
|
b90b70568c | ||
|
23329e7d36 | ||
|
22e2bca83c | ||
|
f67ef7f96f | ||
|
8271737ce2 | ||
d6911626a7 | |||
732dd701af | |||
5300e53c43 | |||
e7fbd53dfe | |||
9cab3142c9 | |||
|
29027bf9f8 | ||
9f6b489bff | |||
|
1333992dc5 | ||
583d2005a7 | |||
51227bfd7b | |||
8276b77c18 | |||
b2f65ba21f | |||
45effe5a33 | |||
83b723f5b9 | |||
8b62b8b7a6 | |||
ff265ce1d4 | |||
|
c9c5837b38 | ||
|
d7bef1ad26 | ||
7b9ce61733 | |||
|
b95b7caefd | ||
49be3c376c |
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
venv/
|
venv/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
init.lock
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
|
41
Makefile
@@ -12,30 +12,53 @@ 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:
|
||||||
|
cd api && \
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
venv-client:
|
||||||
|
cd client && \
|
||||||
|
npm install
|
||||||
|
|
||||||
|
install:
|
||||||
|
make migrate head && \
|
||||||
|
cd api && \
|
||||||
|
poetry run python3 -m api.utils.init
|
||||||
|
|
||||||
%::
|
%::
|
||||||
echo $(MESSAGE)
|
echo $(MESSAGE)
|
||||||
|
|
||||||
|
format-api:
|
||||||
|
cd api && \
|
||||||
|
poetry run ruff format .
|
||||||
|
|
||||||
|
|
||||||
|
check-api:
|
||||||
|
cd api && \
|
||||||
|
poetry run ruff format . --check
|
||||||
|
|
||||||
|
regenerate-openapi-local:
|
||||||
|
cd client \
|
||||||
|
rm src/types/openapi-types.ts \
|
||||||
|
npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts
|
||||||
|
50
README.md
@@ -1 +1,49 @@
|
|||||||
Vorkout/connect
|
# Vorkout/connect
|
||||||
|
|
||||||
|
### Makefile cheat sheet
|
||||||
|
|
||||||
|
```Makefile
|
||||||
|
Dev:
|
||||||
|
venv-api create python virtual environment
|
||||||
|
venv-client install node modules
|
||||||
|
install Migrate database and initialize project
|
||||||
|
|
||||||
|
Application Api:
|
||||||
|
start-api Run api server
|
||||||
|
|
||||||
|
Application Client:
|
||||||
|
start-client Run client server
|
||||||
|
|
||||||
|
Prod:
|
||||||
|
...
|
||||||
|
|
||||||
|
Code:
|
||||||
|
check-api Check api code with ruff
|
||||||
|
format-api Reformat api code with ruff
|
||||||
|
|
||||||
|
Help:
|
||||||
|
...
|
||||||
|
|
||||||
|
Testing:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск в режиме разработки
|
||||||
|
|
||||||
|
Для запуска в режиме разработки нужно
|
||||||
|
|
||||||
|
1. Устрановить среду для clint и api
|
||||||
|
2. Запустить в докере или локально необходимые сервисы (базуб брокер и redis) `make services`
|
||||||
|
3. Для миграции и создания первого пользователя необходимо запустить `make install`
|
||||||
|
3. Запустить api `make start-api`
|
||||||
|
4. Запустить client `make start-client`
|
||||||
|
|
||||||
|
### Миграции алембик
|
||||||
|
|
||||||
|
1. Стоит внимательно учитывать, адрес какой базы стоит в настройках alembic - локальной или продакшн. Посмотреть это можно в файле [env.py](connect/api/api/db/alembic/env.py). Конфиг для локальной базы
|
||||||
|
```python
|
||||||
|
config.set_main_option(
|
||||||
|
"sqlalchemy.url",
|
||||||
|
f"mysql+pymysql://root:hackme@localhost:3306/connect_test",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
import loguru
|
import loguru
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from uvicorn import run
|
from uvicorn import run
|
||||||
|
|
||||||
from api.config import get_settings, DefaultSettings
|
from api.config import DefaultSettings, get_settings
|
||||||
from api.endpoints import list_of_routes
|
from api.endpoints import list_of_routes
|
||||||
|
from api.services.middleware import MiddlewareAccessTokenValidadtion
|
||||||
from api.utils.common import get_hostname
|
from api.utils.common import get_hostname
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
def bind_routes(application: FastAPI, setting: DefaultSettings) -> None:
|
def bind_routes(application: FastAPI, setting: DefaultSettings) -> None:
|
||||||
@@ -32,7 +33,7 @@ def get_app() -> FastAPI:
|
|||||||
description=description,
|
description=description,
|
||||||
docs_url="/swagger",
|
docs_url="/swagger",
|
||||||
openapi_url="/openapi",
|
openapi_url="/openapi",
|
||||||
version="0.1.0",
|
version="0.0.2",
|
||||||
)
|
)
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
bind_routes(application, settings)
|
bind_routes(application, settings)
|
||||||
@@ -44,13 +45,13 @@ app = get_app()
|
|||||||
|
|
||||||
dev_origins = [
|
dev_origins = [
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:64775",
|
||||||
]
|
]
|
||||||
|
|
||||||
prod_origins = [""]
|
prod_origins = [""]
|
||||||
|
|
||||||
origins = dev_origins if get_settings().ENV == "local" else prod_origins
|
origins = dev_origins if get_settings().ENV == "local" else prod_origins
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
settings_for_application = get_settings()
|
settings_for_application = get_settings()
|
||||||
if settings_for_application.ENV == "prod":
|
if settings_for_application.ENV == "prod":
|
||||||
@@ -71,6 +72,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,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
@@ -43,6 +44,32 @@ class DefaultSettings(BaseSettings):
|
|||||||
REDIS_DB: int = int(environ.get("REDIS_DB", "0"))
|
REDIS_DB: int = int(environ.get("REDIS_DB", "0"))
|
||||||
REDIS_PASSWORD: str = environ.get("REDIS_PASSWORD", "hackme")
|
REDIS_PASSWORD: str = environ.get("REDIS_PASSWORD", "hackme")
|
||||||
|
|
||||||
|
SECRET_KEY: str = environ.get("SECRET_KEY", "secret")
|
||||||
|
ALGORITHM: str = environ.get("ALGORITHM", "HS256")
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", 600))
|
||||||
|
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS: int = int(environ.get("REFRESH_TOKEN_EXPIRE_DAYS_LONG", 365))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def database_settings(self) -> dict:
|
||||||
|
"""Get all settings for connection with database."""
|
||||||
|
return {
|
||||||
|
"database": self.MYSQL_DB,
|
||||||
|
"user": self.MYSQL_USER,
|
||||||
|
"password": self.MYSQL_PASSWORD,
|
||||||
|
"host": self.MYSQL_HOST,
|
||||||
|
"port": self.MYSQL_PORT,
|
||||||
|
}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def database_uri(self) -> str:
|
||||||
|
"""Get uri for connection with database."""
|
||||||
|
uri = "mysql+aiomysql://{user}:{password}@{host}:{port}/{database}".format(
|
||||||
|
**self.database_settings,
|
||||||
|
)
|
||||||
|
print("database_uri", uri)
|
||||||
|
return uri
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# env_file = "../.env"
|
# env_file = "../.env"
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
from sqlalchemy import MetaData
|
|
||||||
|
|
||||||
metadata = MetaData()
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"metadata",
|
|
||||||
]
|
|
||||||
|
@@ -7,7 +7,7 @@ from sqlalchemy import pool
|
|||||||
|
|
||||||
from alembic import context
|
from alembic import context
|
||||||
|
|
||||||
from api.db import metadata
|
from orm import metadata, tables
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
# access to the values within the .ini file in use.
|
# access to the values within the .ini file in use.
|
||||||
|
38
api/api/db/alembic/versions/93106fbe7d83_.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 93106fbe7d83
|
||||||
|
Revises: f1b06efacec0
|
||||||
|
Create Date: 2025-06-26 16:36:02.270706
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '93106fbe7d83'
|
||||||
|
down_revision: Union[str, None] = 'f1b06efacec0'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('account_keyring', 'key_value',
|
||||||
|
existing_type=mysql.VARCHAR(length=255),
|
||||||
|
type_=sa.String(length=512),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('account_keyring', 'key_value',
|
||||||
|
existing_type=sa.String(length=512),
|
||||||
|
type_=mysql.VARCHAR(length=255),
|
||||||
|
existing_nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
138
api/api/db/alembic/versions/f1b06efacec0_.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: f1b06efacec0
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-04-23 15:09:14.833213
|
||||||
|
|
||||||
|
"""
|
||||||
|
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 = 'f1b06efacec0'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
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.create_table('account',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('login', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('email', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('bind_tenant_id', sa.String(length=40), nullable=True),
|
||||||
|
sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'EDITOR', 'VIEWER', name='accountrole'), nullable=False),
|
||||||
|
sa.Column('meta', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'BLOCKED', 'DELETED', name='accountstatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('idx_login', 'account', ['login'], unique=False)
|
||||||
|
op.create_index('idx_name', 'account', ['name'], unique=False)
|
||||||
|
op.create_table('account_keyring',
|
||||||
|
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('key_type', sa.Enum('PASSWORD', 'ACCESS_TOKEN', 'REFRESH_TOKEN', 'API_KEY', name='keytype'), nullable=False),
|
||||||
|
sa.Column('key_id', sa.String(length=40), nullable=False),
|
||||||
|
sa.Column('key_value', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('expiry', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'EXPIRED', 'DELETED', name='keystatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('owner_id', 'key_type', 'key_id')
|
||||||
|
)
|
||||||
|
op.create_table('list_events',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=40, collation='latin1_bin'), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('schema', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('state', sa.Enum('AUTO', 'DESCRIPTED', name='eventstate'), nullable=False),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'DELETED', name='eventstatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('process_schema',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=False),
|
||||||
|
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'STOPPING', 'STOPPED', 'DELETED', name='processstatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('idx_owner_id', 'process_schema', ['owner_id'], unique=False)
|
||||||
|
op.create_table('process_version_archive',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('ps_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('version', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('snapshot', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('owner_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('is_last', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['owner_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['ps_id'], ['process_schema.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id', 'version')
|
||||||
|
)
|
||||||
|
op.create_table('ps_node',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('ps_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('node_type', sa.Enum('TYPE1', 'TYPE2', 'TYPE3', name='nodetype'), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'DISABLED', 'DELETED', name='nodestatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['ps_id'], ['process_schema.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('idx_ps_id', 'ps_node', ['ps_id'], unique=False)
|
||||||
|
op.create_table('node_link',
|
||||||
|
sa.Column('id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('link_name', sa.String(length=20), nullable=False),
|
||||||
|
sa.Column('node_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('next_node_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('creator_id', sa.Integer().with_variant(mysql.INTEGER(unsigned=True), 'mysql'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('ACTIVE', 'STOPPING', 'STOPPED', 'DELETED', name='nodelinkstatus'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['next_node_id'], ['ps_node.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['node_id'], ['ps_node.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index('idx_next_node_id', 'node_link', ['next_node_id'], unique=False)
|
||||||
|
op.create_index('idx_node_id', 'node_link', ['node_id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index('idx_node_id', table_name='node_link')
|
||||||
|
op.drop_index('idx_next_node_id', table_name='node_link')
|
||||||
|
op.drop_table('node_link')
|
||||||
|
op.drop_index('idx_ps_id', table_name='ps_node')
|
||||||
|
op.drop_table('ps_node')
|
||||||
|
op.drop_table('process_version_archive')
|
||||||
|
op.drop_index('idx_owner_id', table_name='process_schema')
|
||||||
|
op.drop_table('process_schema')
|
||||||
|
op.drop_table('list_events')
|
||||||
|
op.drop_table('account_keyring')
|
||||||
|
op.drop_index('idx_name', table_name='account')
|
||||||
|
op.drop_index('idx_login', table_name='account')
|
||||||
|
op.drop_table('account')
|
||||||
|
# ### end Alembic commands ###
|
64
api/api/db/connection/session.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import contextlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
from loguru import logger
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
|
||||||
|
from sqlalchemy import URL, create_engine, text
|
||||||
|
|
||||||
|
|
||||||
|
from api.config import get_settings
|
||||||
|
from api.config.default import DbCredentialsSchema
|
||||||
|
|
||||||
|
|
||||||
|
class SessionManager:
|
||||||
|
engines: Any
|
||||||
|
|
||||||
|
def __init__(self, database_uri=get_settings().database_uri) -> None:
|
||||||
|
self.database_uri = database_uri
|
||||||
|
self.refresh(database_uri)
|
||||||
|
# self.reflect()
|
||||||
|
|
||||||
|
def __new__(cls, database_uri=get_settings().database_uri):
|
||||||
|
if not hasattr(cls, "instance"):
|
||||||
|
cls.instance = super(SessionManager, cls).__new__(cls)
|
||||||
|
cls.instance.engines = {}
|
||||||
|
return cls.instance
|
||||||
|
|
||||||
|
def refresh(self, database_uri) -> None:
|
||||||
|
# if not self.engines:
|
||||||
|
# self.engines = {}
|
||||||
|
if database_uri not in self.engines:
|
||||||
|
self.engines[database_uri] = create_async_engine(
|
||||||
|
database_uri,
|
||||||
|
echo=True,
|
||||||
|
future=True,
|
||||||
|
# json_serializer=serializer,
|
||||||
|
pool_recycle=1800,
|
||||||
|
pool_size=get_settings().CONNECTION_POOL_SIZE,
|
||||||
|
max_overflow=get_settings().CONNECTION_OVERFLOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_engine_by_db_uri(self, database_uri) -> AsyncEngine:
|
||||||
|
return self.engines[database_uri]
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def get_connection(
|
||||||
|
database_uri=None,
|
||||||
|
) -> AsyncGenerator[AsyncConnection, None]:
|
||||||
|
if not database_uri:
|
||||||
|
database_uri = get_settings().database_uri
|
||||||
|
engine = SessionManager(database_uri).get_engine_by_db_uri(database_uri)
|
||||||
|
logger.debug(f"engine {engine} {SessionManager(database_uri).engines}")
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
yield conn
|
||||||
|
|
||||||
|
|
||||||
|
async def get_connection_dep() -> AsyncConnection:
|
||||||
|
async with get_connection() as conn:
|
||||||
|
yield conn
|
180
api/api/db/logic/account.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import math
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from orm.tables.account import account_table
|
||||||
|
from api.schemas.account.account import User
|
||||||
|
from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate, UserFilterDTO
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_account_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: UserFilterDTO
|
||||||
|
) -> Optional[AllUserResponse]:
|
||||||
|
"""
|
||||||
|
Получает список пользователей с пагинацией, фильтрацией и сортировкой через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- поиск
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
|
"""
|
||||||
|
|
||||||
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * 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.meta,
|
||||||
|
account_table.c.creator_id,
|
||||||
|
account_table.c.created_at,
|
||||||
|
account_table.c.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
query = query.where(
|
||||||
|
or_(
|
||||||
|
account_table.c.name.ilike(search_term),
|
||||||
|
account_table.c.login.ilike(search_term),
|
||||||
|
account_table.c.email.ilike(search_term),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Фильтрацию
|
||||||
|
filter_conditions = []
|
||||||
|
if filter_dto.filters:
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(account_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
# Сортировка
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
column = getattr(account_table.c, order_field, None)
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(account_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(account_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(
|
||||||
|
account_table.c.name.ilike(search_term),
|
||||||
|
account_table.c.login.ilike(search_term),
|
||||||
|
account_table.c.email.ilike(search_term),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
users_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
|
validated_users = all_user_adapter.validate_python(users_data)
|
||||||
|
|
||||||
|
return AllUserResponse(
|
||||||
|
users=validated_users,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
|
||||||
|
"""
|
||||||
|
Получает юзера по id.
|
||||||
|
"""
|
||||||
|
query = select(account_table).where(account_table.c.id == user_id)
|
||||||
|
|
||||||
|
user_db_cursor = await connection.execute(query)
|
||||||
|
user = user_db_cursor.mappings().one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return User.model_validate(user)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Получает юзера по login.
|
||||||
|
"""
|
||||||
|
query = select(account_table).where(account_table.c.login == login)
|
||||||
|
|
||||||
|
user_db_cursor = await connection.execute(query)
|
||||||
|
user_data = user_db_cursor.mappings().one_or_none()
|
||||||
|
if not user_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return User.model_validate(user_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_user_by_id(connection: AsyncConnection, update_values, user) -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы account_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(account_table.update().where(account_table.c.id == user.id).values(**update_values))
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице account_table.
|
||||||
|
"""
|
||||||
|
query = insert(account_table).values(
|
||||||
|
name=user.name,
|
||||||
|
login=user.login,
|
||||||
|
email=user.email,
|
||||||
|
bind_tenant_id=user.bind_tenant_id,
|
||||||
|
role=user.role.value,
|
||||||
|
meta=user.meta or {},
|
||||||
|
creator_id=creator_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
status=user.status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
new_user = await get_user_by_id(connection, res.lastrowid)
|
||||||
|
|
||||||
|
return new_user
|
87
api/api/db/logic/auth.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from orm.tables.account import account_table, account_keyring_table, KeyType, KeyStatus
|
||||||
|
|
||||||
|
from api.schemas.account.account import User
|
||||||
|
from api.schemas.account.account_keyring import AccountKeyring
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
|
|
||||||
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
|
||||||
|
query = (
|
||||||
|
select(account_table, account_keyring_table)
|
||||||
|
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
|
||||||
|
.where(account_table.c.login == login, account_keyring_table.c.key_type == KeyType.PASSWORD)
|
||||||
|
)
|
||||||
|
|
||||||
|
user_db_cursor = await connection.execute(query)
|
||||||
|
user_db = user_db_cursor.one_or_none()
|
||||||
|
|
||||||
|
if not user_db:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
user_data = {
|
||||||
|
column.name: (
|
||||||
|
getattr(user_db, column.name).name
|
||||||
|
if isinstance(getattr(user_db, column.name), Enum)
|
||||||
|
else getattr(user_db, column.name)
|
||||||
|
)
|
||||||
|
for column in account_table.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
password_data = {
|
||||||
|
column.name: (
|
||||||
|
getattr(user_db, column.name).name
|
||||||
|
if isinstance(getattr(user_db, column.name), Enum)
|
||||||
|
else getattr(user_db, column.name)
|
||||||
|
)
|
||||||
|
for column in account_keyring_table.columns
|
||||||
|
}
|
||||||
|
|
||||||
|
user = AllUser.model_validate(user_data)
|
||||||
|
password = AccountKeyring.model_validate(password_data)
|
||||||
|
return user, password
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade_old_refresh_token(connection: AsyncConnection, refresh_token) -> Optional[User]:
|
||||||
|
new_status = KeyStatus.EXPIRED
|
||||||
|
|
||||||
|
update_query = (
|
||||||
|
update(account_keyring_table)
|
||||||
|
.where(
|
||||||
|
account_keyring_table.c.status == KeyStatus.ACTIVE,
|
||||||
|
account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN,
|
||||||
|
account_keyring_table.c.key_value == refresh_token,
|
||||||
|
)
|
||||||
|
.values(status=new_status)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(update_query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def add_new_refresh_token(
|
||||||
|
connection: AsyncConnection, new_refresh_token, new_refresh_token_expires_time, user
|
||||||
|
) -> Optional[User]:
|
||||||
|
new_refresh_token = account_keyring_table.insert().values(
|
||||||
|
owner_id=user.id,
|
||||||
|
key_type=KeyType.REFRESH_TOKEN,
|
||||||
|
key_id=KeyIdGenerator(),
|
||||||
|
key_value=new_refresh_token,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
expiry=new_refresh_token_expires_time,
|
||||||
|
status=KeyStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(new_refresh_token)
|
||||||
|
|
||||||
|
await connection.commit()
|
95
api/api/db/logic/keyring.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, update
|
||||||
|
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from orm.tables.account import account_keyring_table, KeyStatus, KeyType
|
||||||
|
from api.schemas.account.account_keyring import AccountKeyring
|
||||||
|
from api.utils.hasher import hasher
|
||||||
|
|
||||||
|
|
||||||
|
async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]:
|
||||||
|
"""
|
||||||
|
Получает key по key_id.
|
||||||
|
"""
|
||||||
|
query = select(account_keyring_table).where(account_keyring_table.c.key_id == key_id)
|
||||||
|
|
||||||
|
user_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
user_data = user_db_cursor.mappings().one_or_none()
|
||||||
|
if not user_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return AccountKeyring.model_validate(user_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_key_by_id(connection: AsyncConnection, update_values, key) -> Optional[AccountKeyring]:
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы account_keyring_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(
|
||||||
|
account_keyring_table.update().where(account_keyring_table.c.key_id == key.key_id).values(**update_values)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: int) -> Optional[AccountKeyring]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице account_keyring_table).
|
||||||
|
"""
|
||||||
|
query = insert(account_keyring_table).values(
|
||||||
|
owner_id=key.owner_id,
|
||||||
|
key_type=key.key_type.value,
|
||||||
|
key_id=key_id,
|
||||||
|
key_value=key.key_value,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
expiry=key.expiry,
|
||||||
|
status=key.status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
key.created_at = datetime.now(timezone.utc)
|
||||||
|
key.key_id = key_id
|
||||||
|
|
||||||
|
await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
|
||||||
|
if password is None:
|
||||||
|
password = hasher.generate_password()
|
||||||
|
hashed_password = hasher.hash_data(password)
|
||||||
|
stmt = mysql_insert(account_keyring_table).values(
|
||||||
|
owner_id=owner_id,
|
||||||
|
key_type=KeyType.PASSWORD.value,
|
||||||
|
key_id="PASSWORD",
|
||||||
|
key_value=hashed_password,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
expiry=datetime.now(timezone.utc) + timedelta(days=365),
|
||||||
|
status=KeyStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
stmt.on_duplicate_key_update(key_value=hashed_password)
|
||||||
|
await connection.execute(stmt)
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def update_password_key(connection: AsyncConnection, owner_id: int, password: str):
|
||||||
|
stmt = select(account_keyring_table).where(account_keyring_table.c.owner_id == owner_id)
|
||||||
|
result = await connection.execute(stmt)
|
||||||
|
keyring = result.one_or_none()
|
||||||
|
if not keyring:
|
||||||
|
await create_password_key(connection, password, owner_id)
|
||||||
|
else:
|
||||||
|
stmt = (
|
||||||
|
update(account_keyring_table)
|
||||||
|
.values(key_value=hasher.hash_data(password), expiry=datetime.now(timezone.utc) + timedelta(days=365))
|
||||||
|
.where(account_keyring_table.c.owner_id == owner_id)
|
||||||
|
)
|
||||||
|
await connection.execute(stmt)
|
||||||
|
await connection.commit()
|
279
api/api/db/logic/list_events.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from orm.tables.events import list_events_table
|
||||||
|
|
||||||
|
from api.schemas.events.list_events import ListEvent
|
||||||
|
|
||||||
|
from api.schemas.endpoints.list_events import all_list_event_adapter, AllListEventResponse, ListEventFilterDTO
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: ListEventFilterDTO
|
||||||
|
) -> Optional[AllListEventResponse]:
|
||||||
|
"""
|
||||||
|
Получает список событий с фильтрацией через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- полнотекстовый поиск (пропускает name при русских буквах)
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
|
"""
|
||||||
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
list_events_table.c.id,
|
||||||
|
list_events_table.c.name,
|
||||||
|
list_events_table.c.title,
|
||||||
|
list_events_table.c.creator_id,
|
||||||
|
list_events_table.c.created_at,
|
||||||
|
list_events_table.c.schema.label("schema_"),
|
||||||
|
list_events_table.c.state,
|
||||||
|
list_events_table.c.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
has_russian = any("\u0400" <= char <= "\u04ff" for char in filter_dto.search)
|
||||||
|
|
||||||
|
if has_russian:
|
||||||
|
query = query.where(list_events_table.c.title.ilike(search_term))
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
or_(list_events_table.c.title.ilike(search_term), list_events_table.c.name.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
filter_conditions = []
|
||||||
|
if filter_dto.filters:
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(list_events_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
if order_field.startswith("schema."):
|
||||||
|
json_field = order_field[7:]
|
||||||
|
column = list_events_table.c.schema[json_field].astext
|
||||||
|
else:
|
||||||
|
column = getattr(list_events_table.c, order_field, None)
|
||||||
|
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(list_events_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(list_events_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
has_russian = any("\u0400" <= char <= "\u04ff" for char in filter_dto.search)
|
||||||
|
|
||||||
|
if has_russian:
|
||||||
|
count_query = count_query.where(list_events_table.c.title.ilike(search_term))
|
||||||
|
else:
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(list_events_table.c.title.ilike(search_term), list_events_table.c.name.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
validated_events = all_list_event_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllListEventResponse(
|
||||||
|
list_event=validated_events,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_page_by_creator_id(
|
||||||
|
connection: AsyncConnection, creator_id: int, page: int, limit: int
|
||||||
|
) -> Optional[AllListEventResponse]:
|
||||||
|
"""
|
||||||
|
Получает список событий заданного создателя по значениям page и limit и creator_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
first_event = page * limit - limit
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
list_events_table.c.id,
|
||||||
|
list_events_table.c.name,
|
||||||
|
list_events_table.c.title,
|
||||||
|
list_events_table.c.creator_id,
|
||||||
|
list_events_table.c.created_at,
|
||||||
|
list_events_table.c.schema,
|
||||||
|
list_events_table.c.state,
|
||||||
|
list_events_table.c.status,
|
||||||
|
)
|
||||||
|
.where(list_events_table.c.creator_id == creator_id) # Фильтрация по creator_id
|
||||||
|
.order_by(list_events_table.c.id)
|
||||||
|
.offset(first_event)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
count_query = (
|
||||||
|
select(func.count())
|
||||||
|
.select_from(list_events_table)
|
||||||
|
.where(list_events_table.c.creator_id == creator_id) # Фильтрация по creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
|
# Здесь предполагается, что all_list_event_adapter.validate_python корректно обрабатывает данные
|
||||||
|
validated_list_event = all_list_event_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllListEventResponse(
|
||||||
|
list_event=validated_list_event,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_page(connection: AsyncConnection, page, limit) -> Optional[AllListEventResponse]:
|
||||||
|
"""
|
||||||
|
Получает список событий заданного создателя по значениям page и limit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
first_event = page * limit - (limit)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
list_events_table.c.id,
|
||||||
|
list_events_table.c.name,
|
||||||
|
list_events_table.c.title,
|
||||||
|
list_events_table.c.creator_id,
|
||||||
|
list_events_table.c.created_at,
|
||||||
|
list_events_table.c.schema,
|
||||||
|
list_events_table.c.state,
|
||||||
|
list_events_table.c.status,
|
||||||
|
)
|
||||||
|
.order_by(list_events_table.c.id)
|
||||||
|
.offset(first_event)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(list_events_table)
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
|
# Здесь предполагается, что all_list_event_adapter.validate_python корректно обрабатывает данные
|
||||||
|
validated_list_event = all_list_event_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllListEventResponse(
|
||||||
|
list_event=validated_list_event,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_by_name(connection: AsyncConnection, name: str) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Получает list events по name.
|
||||||
|
"""
|
||||||
|
query = select(list_events_table).where(list_events_table.c.name == name)
|
||||||
|
|
||||||
|
list_events_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
list_events_data = list_events_db_cursor.mappings().one_or_none()
|
||||||
|
if not list_events_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ListEvent.model_validate(list_events_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_list_events_by_id(connection: AsyncConnection, id: int) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Получает listevent по id.
|
||||||
|
"""
|
||||||
|
query = select(list_events_table).where(list_events_table.c.id == id)
|
||||||
|
|
||||||
|
list_events_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
list_events_data = list_events_db_cursor.mappings().one_or_none()
|
||||||
|
if not list_events_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ListEvent.model_validate(list_events_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_list_events_by_id(connection: AsyncConnection, update_values, list_events):
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы list_events_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(
|
||||||
|
list_events_table.update().where(list_events_table.c.id == list_events.id).values(**update_values)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_list_events(
|
||||||
|
connection: AsyncConnection, list_events: ListEvent, creator_id: int
|
||||||
|
) -> Optional[ListEvent]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице list_events_table.
|
||||||
|
"""
|
||||||
|
query = insert(list_events_table).values(
|
||||||
|
name=list_events.name,
|
||||||
|
title=list_events.title, # добавлено поле title
|
||||||
|
creator_id=creator_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
schema=list_events.schema_, # добавлено поле schema
|
||||||
|
state=list_events.state.value, # добавлено поле state
|
||||||
|
status=list_events.status.value, # добавлено поле status
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
return list_events
|
175
api/api/db/logic/process_schema.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select, func, or_, and_, asc, desc
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from orm.tables.process import process_schema_table
|
||||||
|
|
||||||
|
from api.schemas.process.process_schema import ProcessSchema
|
||||||
|
|
||||||
|
from api.schemas.endpoints.process_schema import (
|
||||||
|
all_process_schema_adapter,
|
||||||
|
AllProcessSchemaResponse,
|
||||||
|
ProcessSchemaFilterDTO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_page_DTO(
|
||||||
|
connection: AsyncConnection, filter_dto: ProcessSchemaFilterDTO
|
||||||
|
) -> Optional[AllProcessSchemaResponse]:
|
||||||
|
"""
|
||||||
|
Получает список схем процессов с комплексной фильтрацией через DTO объект.
|
||||||
|
Поддерживает:
|
||||||
|
- пагинацию
|
||||||
|
- поиск
|
||||||
|
- фильтрацию по полям
|
||||||
|
- сортировку
|
||||||
|
"""
|
||||||
|
|
||||||
|
page = filter_dto.pagination.get("page", 1)
|
||||||
|
limit = filter_dto.pagination.get("limit", 10)
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
query = select(
|
||||||
|
process_schema_table.c.id,
|
||||||
|
process_schema_table.c.title,
|
||||||
|
process_schema_table.c.description,
|
||||||
|
process_schema_table.c.owner_id,
|
||||||
|
process_schema_table.c.creator_id,
|
||||||
|
process_schema_table.c.created_at,
|
||||||
|
process_schema_table.c.settings,
|
||||||
|
process_schema_table.c.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
query = query.where(
|
||||||
|
or_(process_schema_table.c.title.ilike(search_term), process_schema_table.c.description.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.filters:
|
||||||
|
filter_conditions = []
|
||||||
|
for field, values in filter_dto.filters.items():
|
||||||
|
column = getattr(process_schema_table.c, field, None)
|
||||||
|
if column is not None and values:
|
||||||
|
if len(values) == 1:
|
||||||
|
filter_conditions.append(column == values[0])
|
||||||
|
else:
|
||||||
|
filter_conditions.append(column.in_(values))
|
||||||
|
|
||||||
|
if filter_conditions:
|
||||||
|
query = query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
if filter_dto.order:
|
||||||
|
order_field = filter_dto.order.get("field", "id")
|
||||||
|
order_direction = filter_dto.order.get("direction", "asc")
|
||||||
|
|
||||||
|
column = getattr(process_schema_table.c, order_field, None)
|
||||||
|
if column is not None:
|
||||||
|
if order_direction.lower() == "desc":
|
||||||
|
query = query.order_by(desc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(asc(column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(process_schema_table.c.id)
|
||||||
|
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
count_query = select(func.count()).select_from(process_schema_table)
|
||||||
|
|
||||||
|
if filter_dto.search:
|
||||||
|
search_term = f"%{filter_dto.search}%"
|
||||||
|
count_query = count_query.where(
|
||||||
|
or_(process_schema_table.c.title.ilike(search_term), process_schema_table.c.description.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_dto.filters and filter_conditions:
|
||||||
|
count_query = count_query.where(and_(*filter_conditions))
|
||||||
|
|
||||||
|
result = await connection.execute(query)
|
||||||
|
count_result = await connection.execute(count_query)
|
||||||
|
|
||||||
|
events_data = result.mappings().all()
|
||||||
|
total_count = count_result.scalar()
|
||||||
|
|
||||||
|
if not total_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
total_pages = math.ceil(total_count / limit)
|
||||||
|
|
||||||
|
validated_process_schema = all_process_schema_adapter.validate_python(events_data)
|
||||||
|
|
||||||
|
return AllProcessSchemaResponse(
|
||||||
|
process_schema=validated_process_schema,
|
||||||
|
amount_count=total_count,
|
||||||
|
amount_pages=total_pages,
|
||||||
|
current_page=page,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_by_title(connection: AsyncConnection, title: str) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Получает process schema по title.
|
||||||
|
"""
|
||||||
|
query = select(process_schema_table).where(process_schema_table.c.title == title)
|
||||||
|
|
||||||
|
process_schema_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
process_schema_data = process_schema_db_cursor.mappings().one_or_none()
|
||||||
|
if not process_schema_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ProcessSchema.model_validate(process_schema_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_process_schema_by_id(connection: AsyncConnection, id: int) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Получает process_schema по id.
|
||||||
|
"""
|
||||||
|
query = select(process_schema_table).where(process_schema_table.c.id == id)
|
||||||
|
|
||||||
|
process_schema_db_cursor = await connection.execute(query)
|
||||||
|
|
||||||
|
process_schema_data = process_schema_db_cursor.mappings().one_or_none()
|
||||||
|
if not process_schema_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return ProcessSchema.model_validate(process_schema_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_process_schema_by_id(connection: AsyncConnection, update_values, process_schema):
|
||||||
|
"""
|
||||||
|
Вносит изменеия в нужное поле таблицы process_schema_table.
|
||||||
|
"""
|
||||||
|
await connection.execute(
|
||||||
|
process_schema_table.update().where(process_schema_table.c.id == process_schema.id).values(**update_values)
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def create_process_schema(
|
||||||
|
connection: AsyncConnection, process_schema: ProcessSchema, creator_id: int
|
||||||
|
) -> Optional[ProcessSchema]:
|
||||||
|
"""
|
||||||
|
Создает нове поле в таблице process_schema_table.
|
||||||
|
"""
|
||||||
|
query = insert(process_schema_table).values(
|
||||||
|
title=process_schema.title,
|
||||||
|
description=process_schema.description,
|
||||||
|
owner_id=process_schema.owner_id,
|
||||||
|
creator_id=creator_id,
|
||||||
|
created_at=datetime.now(timezone.utc),
|
||||||
|
settings=process_schema.settings,
|
||||||
|
status=process_schema.status.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
await connection.execute(query)
|
||||||
|
|
||||||
|
await connection.commit()
|
||||||
|
|
||||||
|
return process_schema
|
@@ -1,4 +1,11 @@
|
|||||||
list_of_routes = []
|
from api.endpoints.auth import api_router as auth_router
|
||||||
|
from api.endpoints.profile import api_router as profile_router
|
||||||
|
from api.endpoints.account import api_router as account_router
|
||||||
|
from api.endpoints.keyring import api_router as keyring_router
|
||||||
|
from api.endpoints.list_events import api_router as listevents_router
|
||||||
|
from api.endpoints.process_schema import api_router as processschema_router
|
||||||
|
|
||||||
|
list_of_routes = [auth_router, profile_router, account_router, keyring_router, listevents_router, processschema_router]
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"list_of_routes",
|
"list_of_routes",
|
||||||
|
151
api/api/endpoints/account.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from orm.tables.account import AccountStatus
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.db.logic.account import (
|
||||||
|
create_user,
|
||||||
|
get_user_account_page_DTO,
|
||||||
|
get_user_by_id,
|
||||||
|
get_user_by_login,
|
||||||
|
update_user_by_id,
|
||||||
|
)
|
||||||
|
from api.db.logic.keyring import create_password_key, update_password_key
|
||||||
|
from api.schemas.account.account import User
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
from api.schemas.endpoints.account import AllUserResponse, UserCreate, UserFilterDTO, UserUpdate
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
from api.services.user_role_validation import db_user_role_validation
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/account",
|
||||||
|
tags=["User accountModel"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllUserResponse)
|
||||||
|
async def get_all_account(
|
||||||
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
|
limit: int = Query(10, description="КNumber of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by name or login or email"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
role_filter: Optional[List[str]] = Query(None, description="Filter by role"),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"role": role_filter} if role_filter else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = UserFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_list = await get_user_account_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
|
if user_list is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Accounts not found")
|
||||||
|
|
||||||
|
return user_list
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
|
async def get_account(
|
||||||
|
user_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
|
async def create_account(
|
||||||
|
user: UserCreate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
user_validation = await get_user_by_login(connection, user.login)
|
||||||
|
|
||||||
|
if user_validation is None:
|
||||||
|
new_user = await create_user(connection, user, authorize_user.id)
|
||||||
|
await create_password_key(connection, user.password, new_user.id)
|
||||||
|
return new_user
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
|
||||||
|
async def update_account(
|
||||||
|
user_id: int,
|
||||||
|
user_update: UserUpdate,
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return user
|
||||||
|
|
||||||
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
|
async def delete_account(
|
||||||
|
user_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
|
|
||||||
|
user_update = UserUpdate(status=AccountStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return user
|
||||||
|
|
||||||
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
|
user = await get_user_by_id(connection, user_id)
|
||||||
|
|
||||||
|
return user
|
106
api/api/endpoints/auth.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
Response,
|
||||||
|
status,
|
||||||
|
Request,
|
||||||
|
)
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from fastapi_jwt_auth import AuthJWT
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.config import get_settings
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.services.auth import authenticate_user
|
||||||
|
|
||||||
|
from api.db.logic.auth import add_new_refresh_token, upgrade_old_refresh_token
|
||||||
|
|
||||||
|
from api.schemas.endpoints.auth import Auth, Tokens
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/auth",
|
||||||
|
tags=["User auth"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseModel):
|
||||||
|
authjwt_secret_key: str = get_settings().SECRET_KEY
|
||||||
|
# Configure application to store and get JWT from cookies
|
||||||
|
authjwt_token_location: set = {"headers"}
|
||||||
|
authjwt_cookie_domain: str = get_settings().DOMAIN
|
||||||
|
|
||||||
|
# Only allow JWT cookies to be sent over https
|
||||||
|
authjwt_cookie_secure: bool = get_settings().ENV == "prod"
|
||||||
|
# Enable csrf double submit protection. default is True
|
||||||
|
authjwt_cookie_csrf_protect: bool = False
|
||||||
|
authjwt_cookie_samesite: str = "lax"
|
||||||
|
|
||||||
|
|
||||||
|
@AuthJWT.load_config
|
||||||
|
def get_config():
|
||||||
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", response_model=Tokens)
|
||||||
|
async def login_for_access_token(
|
||||||
|
user: Auth,
|
||||||
|
response: Response,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
Authorize: AuthJWT = Depends(),
|
||||||
|
):
|
||||||
|
"""Авторизирует, выставляет токены в куки."""
|
||||||
|
|
||||||
|
user = await authenticate_user(connection, user.login, user.password)
|
||||||
|
|
||||||
|
# print("login_for_access_token", user)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
# headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS)
|
||||||
|
|
||||||
|
logger.debug(f"refresh_token_expires {refresh_token_expires}")
|
||||||
|
|
||||||
|
access_token = Authorize.create_access_token(subject=user.login, expires_time=access_token_expires)
|
||||||
|
refresh_token = Authorize.create_refresh_token(subject=user.login, expires_time=refresh_token_expires)
|
||||||
|
|
||||||
|
refresh_token_expires_time = datetime.now(timezone.utc) + refresh_token_expires
|
||||||
|
|
||||||
|
await add_new_refresh_token(connection, refresh_token, refresh_token_expires_time, user)
|
||||||
|
|
||||||
|
return Tokens(access_token=access_token, refresh_token=refresh_token)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/refresh", response_model=Tokens)
|
||||||
|
async def refresh(
|
||||||
|
request: Request,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
Authorize: AuthJWT = Depends(),
|
||||||
|
) -> Tokens:
|
||||||
|
try:
|
||||||
|
Authorize.jwt_refresh_token_required()
|
||||||
|
current_user = Authorize.get_jwt_subject()
|
||||||
|
except Exception:
|
||||||
|
refresh_token = request.headers.get("Authorization").split(" ")[1]
|
||||||
|
await upgrade_old_refresh_token(connection, refresh_token)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid refresh token",
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return Tokens(access_token=new_access_token)
|
114
api/api/endpoints/keyring.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from orm.tables.account import KeyStatus
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.db.logic.keyring import create_key, get_key_by_id, update_key_by_id
|
||||||
|
from api.schemas.account.account_keyring import AccountKeyring
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
from api.schemas.endpoints.account_keyring import AccountKeyringUpdate
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
from api.services.user_role_validation import db_user_role_validation
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/keyring",
|
||||||
|
tags=["User KeyringModel"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
|
||||||
|
async def get_keyring(
|
||||||
|
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)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
|
if keyring is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Key not found")
|
||||||
|
|
||||||
|
return keyring
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
|
||||||
|
async def create_keyring(
|
||||||
|
user_id: int,
|
||||||
|
key_id: str,
|
||||||
|
key: AccountKeyringUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
|
if keyring is None:
|
||||||
|
keyring_new = await create_key(
|
||||||
|
connection,
|
||||||
|
key,
|
||||||
|
key_id,
|
||||||
|
)
|
||||||
|
return keyring_new
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An keyring with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
|
||||||
|
async def update_keyring(
|
||||||
|
user_id: int,
|
||||||
|
key_id: str,
|
||||||
|
keyring_update: AccountKeyringUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
authorize_user = await db_user_role_validation(connection, current_user)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
if keyring is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
|
||||||
|
|
||||||
|
updated_values = keyring_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return keyring
|
||||||
|
|
||||||
|
await update_key_by_id(connection, updated_values, keyring)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
|
return keyring
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{user_id}/{key_id}", dependencies=[Depends(bearer_schema)], response_model=AccountKeyring)
|
||||||
|
async def delete_keyring(
|
||||||
|
user_id: int,
|
||||||
|
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)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
if keyring is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="keyring not found")
|
||||||
|
|
||||||
|
keyring_update = AccountKeyringUpdate(status=KeyStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = keyring_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return keyring
|
||||||
|
|
||||||
|
await update_key_by_id(connection, updated_values, keyring)
|
||||||
|
|
||||||
|
keyring = await get_key_by_id(connection, key_id)
|
||||||
|
|
||||||
|
return keyring
|
169
api/api/endpoints/list_events.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from orm.tables.events import EventStatus
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.db.logic.account import get_user_by_login
|
||||||
|
from api.db.logic.list_events import (
|
||||||
|
create_list_events,
|
||||||
|
get_list_events_by_id,
|
||||||
|
get_list_events_by_name,
|
||||||
|
get_list_events_page_DTO,
|
||||||
|
update_list_events_by_id,
|
||||||
|
)
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
from api.schemas.endpoints.list_events import AllListEventResponse, ListEventFilterDTO, ListEventUpdate
|
||||||
|
from api.schemas.events.list_events import ListEvent
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
from api.services.user_role_validation import (
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema,
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/list_events",
|
||||||
|
tags=["list events"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllListEventResponse)
|
||||||
|
async def get_all_list_events(
|
||||||
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
|
limit: int = Query(10, description="Number of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by title or name"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
state_filter: Optional[List[str]] = Query(None, description="Filter by state"),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"state": state_filter} if state_filter else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = ListEventFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
authorize_user, page_flag = await db_user_role_validation_for_list_events_and_process_schema(
|
||||||
|
connection, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not page_flag:
|
||||||
|
if filter_dto.filters is None:
|
||||||
|
filter_dto.filters = {}
|
||||||
|
filter_dto.filters["creator_id"] = [str(authorize_user.id)]
|
||||||
|
|
||||||
|
list_events_page = await get_list_events_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
|
if list_events_page is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
return list_events_page
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def get_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if list_events_id is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def create_list_events(
|
||||||
|
list_events: ListEventUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user_validation = await get_user_by_login(connection, current_user)
|
||||||
|
list_events_validation = await get_list_events_by_name(connection, list_events.name)
|
||||||
|
|
||||||
|
if list_events_validation is None:
|
||||||
|
await create_list_events(connection, list_events, user_validation.id)
|
||||||
|
list_events_new = await get_list_events_by_name(connection, list_events.name)
|
||||||
|
return list_events_new
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An List events with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def update_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
list_events_update: ListEventUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_values = list_events_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
await update_list_events_by_id(connection, updated_values, list_events_validation)
|
||||||
|
|
||||||
|
list_events = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
return list_events
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{list_events_id}", dependencies=[Depends(bearer_schema)], response_model=ListEvent)
|
||||||
|
async def delete_list_events(
|
||||||
|
list_events_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
list_events_validation = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
if list_events_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="List events not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, list_events_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
list_events_update = ListEventUpdate(status=EventStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = list_events_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return list_events_validation
|
||||||
|
|
||||||
|
await update_list_events_by_id(connection, updated_values, list_events_validation)
|
||||||
|
|
||||||
|
list_events = await get_list_events_by_id(connection, list_events_id)
|
||||||
|
|
||||||
|
return list_events
|
169
api/api/endpoints/process_schema.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from orm.tables.process import ProcessStatus
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.db.logic.account import get_user_by_login
|
||||||
|
from api.db.logic.process_schema import (
|
||||||
|
create_process_schema,
|
||||||
|
get_process_schema_by_id,
|
||||||
|
get_process_schema_by_title,
|
||||||
|
get_process_schema_page_DTO,
|
||||||
|
update_process_schema_by_id,
|
||||||
|
)
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
from api.schemas.endpoints.process_schema import AllProcessSchemaResponse, ProcessSchemaFilterDTO, ProcessSchemaUpdate
|
||||||
|
from api.schemas.process.process_schema import ProcessSchema
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
from api.services.user_role_validation import (
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema,
|
||||||
|
db_user_role_validation_for_list_events_and_process_schema_by_list_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/process_schema",
|
||||||
|
tags=["process schema"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=AllProcessSchemaResponse)
|
||||||
|
async def get_all_process_schema(
|
||||||
|
page: int = Query(1, description="Page number", gt=0),
|
||||||
|
limit: int = Query(10, description="Number of items per page", gt=0),
|
||||||
|
search: Optional[str] = Query(None, description="Search term to filter by title or description"),
|
||||||
|
order_field: Optional[str] = Query("id", description="Field to sort by"),
|
||||||
|
order_direction: Optional[str] = Query("asc", description="Sort direction (asc/desc)"),
|
||||||
|
status_filter: Optional[List[str]] = Query(None, description="Filter by status"),
|
||||||
|
owner_id: Optional[List[str]] = Query(None, description="Filter by owner id"),
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
creator_id: Optional[int] = Query(None, description="Filter by creator id"),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
filters = {
|
||||||
|
**({"status": status_filter} if status_filter else {}),
|
||||||
|
**({"owner_id": owner_id} if owner_id else {}),
|
||||||
|
**({"creator_id": [str(creator_id)]} if creator_id else {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
filter_dto = ProcessSchemaFilterDTO(
|
||||||
|
pagination={"page": page, "limit": limit},
|
||||||
|
search=search,
|
||||||
|
order={"field": order_field, "direction": order_direction},
|
||||||
|
filters=filters if filters else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
authorize_user, page_flag = await db_user_role_validation_for_list_events_and_process_schema(
|
||||||
|
connection, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not page_flag:
|
||||||
|
if filter_dto.filters is None:
|
||||||
|
filter_dto.filters = {}
|
||||||
|
filter_dto.filters["creator_id"] = [str(authorize_user.id)]
|
||||||
|
|
||||||
|
process_schema_page = await get_process_schema_page_DTO(connection, filter_dto)
|
||||||
|
|
||||||
|
if process_schema_page is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
return process_schema_page
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def get_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if process_schema_id is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def create_processschema(
|
||||||
|
process_schema: ProcessSchemaUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user_validation = await get_user_by_login(connection, current_user)
|
||||||
|
process_schema_validation = await get_process_schema_by_title(connection, process_schema.title)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
await create_process_schema(connection, process_schema, user_validation.id)
|
||||||
|
process_schema_new = await get_process_schema_by_title(connection, process_schema.title)
|
||||||
|
return process_schema_new
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="An process schema with this information already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def update_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
process_schema_update: ProcessSchemaUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_values = process_schema_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
await update_process_schema_by_id(connection, updated_values, process_schema_validation)
|
||||||
|
|
||||||
|
process_schema = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
return process_schema
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.delete("/{process_schema_id}", dependencies=[Depends(bearer_schema)], response_model=ProcessSchema)
|
||||||
|
async def delete_process_schema(
|
||||||
|
process_schema_id: int,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
process_schema_validation = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
if process_schema_validation is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Process schema not found")
|
||||||
|
|
||||||
|
authorize_user = await db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, process_schema_validation.creator_id
|
||||||
|
)
|
||||||
|
|
||||||
|
process_schema_update = ProcessSchemaUpdate(status=ProcessStatus.DELETED.value)
|
||||||
|
|
||||||
|
updated_values = process_schema_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if not updated_values:
|
||||||
|
return process_schema_validation
|
||||||
|
|
||||||
|
await update_process_schema_by_id(connection, updated_values, process_schema_validation)
|
||||||
|
|
||||||
|
process_schema = await get_process_schema_by_id(connection, process_schema_id)
|
||||||
|
|
||||||
|
return process_schema
|
56
api/api/endpoints/profile.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from fastapi import (
|
||||||
|
APIRouter,
|
||||||
|
Depends,
|
||||||
|
HTTPException,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection_dep
|
||||||
|
from api.db.logic.account import get_user_by_id, get_user_by_login, update_user_by_id
|
||||||
|
from api.schemas.account.account import User
|
||||||
|
from api.schemas.base import bearer_schema
|
||||||
|
from api.schemas.endpoints.account import UserUpdate
|
||||||
|
from api.services.auth import get_current_user
|
||||||
|
|
||||||
|
api_router = APIRouter(
|
||||||
|
prefix="/profile",
|
||||||
|
tags=["User accountModel"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.get("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
|
async def get_profile(
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
|
||||||
|
):
|
||||||
|
user = await get_user_by_login(connection, current_user)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.put("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||||
|
async def update_profile(
|
||||||
|
user_update: UserUpdate,
|
||||||
|
connection: AsyncConnection = Depends(get_connection_dep),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
user = await get_user_by_login(connection, current_user)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
|
|
||||||
|
if user_update.role is None and user_update.login is None:
|
||||||
|
updated_values = user_update.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
|
||||||
|
if updated_values is None:
|
||||||
|
return user
|
||||||
|
|
||||||
|
await update_user_by_id(connection, updated_values, user)
|
||||||
|
|
||||||
|
user = await get_user_by_id(connection, user.id)
|
||||||
|
|
||||||
|
return user
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Bad body")
|
20
api/api/schemas/account/account.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from orm.tables.account import AccountRole, AccountStatus
|
||||||
|
from pydantic import EmailStr, Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
id: Optional[int] = None
|
||||||
|
name: str = Field(..., max_length=100)
|
||||||
|
login: str = Field(..., max_length=100)
|
||||||
|
email: Optional[EmailStr] = Field(None, max_length=100) # Электронная почта (может быть None)
|
||||||
|
bind_tenant_id: Optional[str] = Field(None, max_length=40)
|
||||||
|
role: AccountRole
|
||||||
|
meta: dict
|
||||||
|
creator_id: Optional[int] = None
|
||||||
|
created_at: datetime
|
||||||
|
status: AccountStatus
|
17
api/api/schemas/account/account_keyring.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from orm.tables.account import KeyStatus, KeyType
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class AccountKeyring(Base):
|
||||||
|
owner_id: int
|
||||||
|
key_type: KeyType
|
||||||
|
key_id: Optional[str] = Field(None, max_length=40)
|
||||||
|
key_value: str = Field(..., max_length=255)
|
||||||
|
created_at: datetime
|
||||||
|
expiry: Optional[datetime] = None
|
||||||
|
status: KeyStatus
|
14
api/api/schemas/base.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from fastapi.security import HTTPBearer
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from pydantic.alias_generators import to_camel
|
||||||
|
|
||||||
|
|
||||||
|
bearer_schema = HTTPBearer() # схема для авторизации в swagger
|
||||||
|
|
||||||
|
|
||||||
|
class Base(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
from_attributes=True,
|
||||||
|
alias_generator=to_camel,
|
||||||
|
populate_by_name=True,
|
||||||
|
)
|
0
api/api/schemas/endpoints/__init__.py
Normal file
60
api/api/schemas/endpoints/account.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from orm.tables.account import AccountRole, AccountStatus
|
||||||
|
from pydantic import EmailStr, Field, TypeAdapter
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class UserUpdate(Base):
|
||||||
|
name: Optional[str] = Field(None, max_length=100)
|
||||||
|
login: Optional[str] = Field(None, max_length=100)
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
password: Optional[str] = None
|
||||||
|
bind_tenant_id: Optional[str] = Field(None, max_length=40)
|
||||||
|
role: Optional[AccountRole] = None
|
||||||
|
meta: Optional[dict] = None
|
||||||
|
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
|
||||||
|
meta: Optional[dict] = None
|
||||||
|
creator_id: Optional[int] = None
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
class UserFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
13
api/api/schemas/endpoints/account_keyring.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from orm.tables.account import KeyStatus, KeyType
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class AccountKeyringUpdate(Base):
|
||||||
|
owner_id: Optional[int] = None
|
||||||
|
key_type: Optional[KeyType] = None
|
||||||
|
key_value: Optional[str] = Field(None, max_length=255)
|
||||||
|
status: Optional[KeyStatus] = None
|
14
api/api/schemas/endpoints/auth.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
# Таблица для получения информации из запроса
|
||||||
|
|
||||||
|
|
||||||
|
class Auth(Base):
|
||||||
|
login: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class Tokens(Base):
|
||||||
|
access_token: str
|
||||||
|
refresh_token: str | None = None
|
44
api/api/schemas/endpoints/list_events.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from orm.tables.events import EventState, EventStatus
|
||||||
|
from pydantic import Field, TypeAdapter
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ListEventUpdate(Base):
|
||||||
|
name: Optional[str] = Field(None, max_length=40)
|
||||||
|
title: Optional[str] = Field(None, max_length=64)
|
||||||
|
schema_: Optional[Dict[str, Any]] = Field(None, alias="schema")
|
||||||
|
state: Optional[EventState] = None
|
||||||
|
status: Optional[EventStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AllListEvent(Base):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
title: str
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
schema_: Dict[str, Any] = Field(default={}, alias="schema")
|
||||||
|
state: EventState
|
||||||
|
status: EventStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AllListEventResponse(Base):
|
||||||
|
list_event: List[AllListEvent]
|
||||||
|
amount_count: int
|
||||||
|
amount_pages: int
|
||||||
|
current_page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
all_list_event_adapter = TypeAdapter(List[AllListEvent])
|
||||||
|
|
||||||
|
|
||||||
|
class ListEventFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
44
api/api/schemas/endpoints/process_schema.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from orm.tables.process import ProcessStatus
|
||||||
|
from pydantic import Field, TypeAdapter
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSchemaUpdate(Base):
|
||||||
|
title: Optional[str] = Field(None, max_length=100)
|
||||||
|
description: Optional[str] = None
|
||||||
|
owner_id: Optional[int] = None
|
||||||
|
settings: Optional[Dict[str, Any]] = None
|
||||||
|
status: Optional[ProcessStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AllProcessSchema(Base):
|
||||||
|
id: int
|
||||||
|
title: str = Field(..., max_length=100)
|
||||||
|
description: str
|
||||||
|
owner_id: int
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
settings: Dict[str, Any]
|
||||||
|
status: ProcessStatus
|
||||||
|
|
||||||
|
|
||||||
|
class AllProcessSchemaResponse(Base):
|
||||||
|
process_schema: List[AllProcessSchema]
|
||||||
|
amount_count: int
|
||||||
|
amount_pages: int
|
||||||
|
current_page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
|
||||||
|
all_process_schema_adapter = TypeAdapter(List[AllProcessSchema])
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSchemaFilterDTO(Base):
|
||||||
|
pagination: Dict[str, int]
|
||||||
|
search: Optional[str] = None
|
||||||
|
order: Optional[Dict[str, str]] = None
|
||||||
|
filters: Optional[Dict[str, List[str]]] = None
|
0
api/api/schemas/events/__init__.py
Normal file
18
api/api/schemas/events/list_events.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from orm.tables.events import EventState, EventStatus
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ListEvent(Base):
|
||||||
|
id: int
|
||||||
|
name: str = Field(..., max_length=40)
|
||||||
|
title: str = Field(..., max_length=64)
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
schema_: Dict[str, Any] = Field(..., alias="schema")
|
||||||
|
state: EventState
|
||||||
|
status: EventStatus
|
0
api/api/schemas/process/__init__.py
Normal file
18
api/api/schemas/process/node_link.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from orm.tables.process import NodeStatus
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class MyModel(Base):
|
||||||
|
id: int
|
||||||
|
link_name: str = Field(..., max_length=20)
|
||||||
|
node_id: int
|
||||||
|
next_node_id: int
|
||||||
|
settings: Dict[str, Any]
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
status: NodeStatus
|
18
api/api/schemas/process/process_schema.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from orm.tables.process import ProcessStatus
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSchema(Base):
|
||||||
|
id: int
|
||||||
|
title: str = Field(..., max_length=100)
|
||||||
|
description: str
|
||||||
|
owner_id: int
|
||||||
|
creator_id: int
|
||||||
|
created_at: datetime
|
||||||
|
settings: Dict[str, Any]
|
||||||
|
status: ProcessStatus
|
13
api/api/schemas/process/process_version_archive.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessStatusSchema(Base):
|
||||||
|
id: int
|
||||||
|
version: int
|
||||||
|
snapshot: Dict[str, Any]
|
||||||
|
owner_id: int
|
||||||
|
created_at: datetime
|
||||||
|
is_last: int
|
16
api/api/schemas/process/ps_node.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from orm.tables.process import NodeStatus, NodeType
|
||||||
|
|
||||||
|
from api.schemas.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Ps_Node(Base):
|
||||||
|
id: int
|
||||||
|
ps_id: int
|
||||||
|
node_type: NodeType
|
||||||
|
settings: dict
|
||||||
|
creator_id: Dict[str, Any]
|
||||||
|
created_at: datetime
|
||||||
|
status: NodeStatus
|
25
api/api/services/auth.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from orm.tables.account import AccountStatus
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||||
|
|
||||||
|
from api.db.logic.auth import get_user
|
||||||
|
from api.schemas.endpoints.account import AllUser
|
||||||
|
from api.utils.hasher import hasher
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
||||||
|
return None
|
||||||
|
if not hasher.verify_data(password, sql_password.key_value):
|
||||||
|
return None
|
||||||
|
return sql_user
|
53
api/api/services/middleware.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import re
|
||||||
|
from re import escape
|
||||||
|
|
||||||
|
from fastapi import (
|
||||||
|
Request,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi_jwt_auth import AuthJWT
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
from api.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware):
|
||||||
|
def __init__(self, app):
|
||||||
|
super().__init__(app)
|
||||||
|
|
||||||
|
self.prefix = escape(get_settings().PATH_PREFIX)
|
||||||
|
self.excluded_routes = [
|
||||||
|
re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"),
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
return await call_next(request)
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
if not auth_header:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content={"detail": "Missing authorization header."},
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
token = auth_header.split(" ")[1]
|
||||||
|
Authorize = AuthJWT(request)
|
||||||
|
current_user = Authorize.get_jwt_subject()
|
||||||
|
request.state.current_user = current_user
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
content={"detail": "The access token is invalid or expired."},
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return await call_next(request)
|
32
api/api/services/user_role_validation.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import (
|
||||||
|
HTTPException,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
from orm.tables.account import AccountRole
|
||||||
|
|
||||||
|
from api.db.logic.account import get_user_by_login
|
||||||
|
|
||||||
|
|
||||||
|
async def db_user_role_validation(connection, current_user):
|
||||||
|
authorize_user = await get_user_by_login(connection, current_user)
|
||||||
|
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions")
|
||||||
|
return authorize_user
|
||||||
|
|
||||||
|
|
||||||
|
async def db_user_role_validation_for_list_events_and_process_schema_by_list_event_id(
|
||||||
|
connection, current_user, current_listevents_creator_id
|
||||||
|
):
|
||||||
|
authorize_user = await get_user_by_login(connection, current_user)
|
||||||
|
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
|
||||||
|
if authorize_user.id != current_listevents_creator_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You do not have enough permissions")
|
||||||
|
return authorize_user
|
||||||
|
|
||||||
|
|
||||||
|
async def db_user_role_validation_for_list_events_and_process_schema(connection, current_user):
|
||||||
|
authorize_user = await get_user_by_login(connection, current_user)
|
||||||
|
if authorize_user.role not in {AccountRole.OWNER, AccountRole.ADMIN}:
|
||||||
|
return authorize_user, False
|
||||||
|
else:
|
||||||
|
return authorize_user, True
|
25
api/api/utils/hasher.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
|
# Хешер для работы с паролем.
|
||||||
|
|
||||||
|
|
||||||
|
class Hasher:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def hash_data(self, password: str) -> str:
|
||||||
|
# Хеширует пароль с использованием SHA-256.
|
||||||
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
def verify_data(self, password: str, hashed: str) -> bool:
|
||||||
|
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
||||||
|
return self.hash_data(password) == hashed
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_password() -> str:
|
||||||
|
return secrets.token_urlsafe(20)
|
||||||
|
|
||||||
|
|
||||||
|
hasher = Hasher()
|
54
api/api/utils/init.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
|
||||||
|
from orm.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
|
||||||
|
|
||||||
|
from api.db.connection.session import get_connection
|
||||||
|
from api.utils.hasher import hasher
|
||||||
|
from api.utils.key_id_gen import KeyIdGenerator
|
||||||
|
|
||||||
|
INIT_LOCK_FILE = "../init.lock"
|
||||||
|
DEFAULT_LOGIN = "vorkout"
|
||||||
|
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
if os.path.exists(INIT_LOCK_FILE):
|
||||||
|
print("Sorry, service is already initialized")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with get_connection() as conn:
|
||||||
|
password = hasher.generate_password()
|
||||||
|
hashed_password = hasher.hash_data(password)
|
||||||
|
|
||||||
|
create_user_query = account_table.insert().values(
|
||||||
|
name=DEFAULT_LOGIN,
|
||||||
|
login=DEFAULT_LOGIN,
|
||||||
|
role=AccountRole.OWNER,
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await conn.execute(create_user_query)
|
||||||
|
user_id = res.lastrowid
|
||||||
|
|
||||||
|
create_key_query = account_keyring_table.insert().values(
|
||||||
|
owner_id=user_id,
|
||||||
|
key_type=KeyType.PASSWORD,
|
||||||
|
key_id=KeyIdGenerator(),
|
||||||
|
key_value=hashed_password,
|
||||||
|
status=KeyStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
await conn.execute(create_key_query)
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
with open(INIT_LOCK_FILE, "w") as lock_file:
|
||||||
|
lock_file.write("initialized\n")
|
||||||
|
|
||||||
|
print(f"Login: {DEFAULT_LOGIN}")
|
||||||
|
print(f"Password: {password}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.get_event_loop().run_until_complete(init())
|
10
api/api/utils/key_id_gen.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Генератор key_id для таблицы account_keyring
|
||||||
|
|
||||||
|
|
||||||
|
def KeyIdGenerator():
|
||||||
|
random_number = random.randint(1000, 9999)
|
||||||
|
result = f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}"
|
||||||
|
return result
|
1066
api/poetry.lock
generated
@@ -1,24 +1,33 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.0.1"
|
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)",
|
||||||
"cryptography (>=44.0.2,<45.0.0)",
|
"cryptography (>=44.0.2,<45.0.0)",
|
||||||
|
"pydantic[email] (>=2.11.3,<3.0.0)",
|
||||||
|
"python-multipart (>=0.0.20,<0.0.21)",
|
||||||
|
"fastapi-jwt-auth @ git+https://github.com/vvpreo/fastapi-jwt-auth",
|
||||||
|
"core-library @ git+https://gitea.heado.ru/Vorkout/core.git@0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
ruff = "^0.11.10"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120
|
||||||
|
extend-exclude = ["alembic"]
|
||||||
|
@@ -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
@@ -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>
|
18319
client/package-lock.json
generated
@@ -1,27 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.0.1",
|
"version": "0.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.6.1",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@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",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"axios-retry": "^4.5.0",
|
||||||
|
"i18next": "^25.0.1",
|
||||||
|
"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-scripts": "5.0.1",
|
"react-i18next": "^15.5.1",
|
||||||
"typescript": "^4.9.5",
|
"react-router-dom": "^7.5.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4",
|
||||||
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"dev": "vite",
|
||||||
"build": "react-scripts build",
|
"build": "vite build",
|
||||||
"test": "react-scripts test",
|
"preview": "vite preview"
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -40,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
After Width: | Height: | Size: 15 KiB |
3
client/public/icons/drawer/add_photo_alternate.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2 18C1.45 18 0.979167 17.8042 0.5875 17.4125C0.195833 17.0208 0 16.55 0 16V2C0 1.45 0.195833 0.979167 0.5875 0.5875C0.979167 0.195833 1.45 0 2 0H10V2H2V16H16V8H18V16C18 16.55 17.8042 17.0208 17.4125 17.4125C17.0208 17.8042 16.55 18 16 18H2ZM3 14H15L11.25 9L8.25 13L6 10L3 14ZM14 6V4H12V2H14V0H16V2H18V4H16V6H14Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 443 B |
3
client/public/icons/drawer/arrow_back.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.825 9L9.425 14.6L8 16L0 8L8 0L9.425 1.4L3.825 7H16V9H3.825Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 193 B |
3
client/public/icons/drawer/delete.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 18C2.45 18 1.97917 17.8042 1.5875 17.4125C1.19583 17.0208 1 16.55 1 16V3H0V1H5V0H11V1H16V3H15V16C15 16.55 14.8042 17.0208 14.4125 17.4125C14.0208 17.8042 13.55 18 13 18H3ZM13 3H3V16H13V3ZM5 14H7V5H5V14ZM9 14H11V5H9V14Z" fill="#FF0000"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 352 B |
3
client/public/icons/drawer/reg.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="21" height="17" viewBox="0 0 21 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.5 16V13.2C0.5 12.65 0.641667 12.1333 0.925 11.65C1.20833 11.1667 1.6 10.8 2.1 10.55C2.95 10.1167 3.90833 9.75 4.975 9.45C6.04167 9.15 7.21667 9 8.5 9C9 9 9.4875 9.025 9.9625 9.075C10.4375 9.125 10.9 9.2 11.35 9.3L9.6 11.05C9.41667 11.0167 9.2375 11 9.0625 11H8.5C7.31667 11 6.25417 11.1417 5.3125 11.425C4.37083 11.7083 3.6 12.0167 3 12.35C2.85 12.4333 2.72917 12.55 2.6375 12.7C2.54583 12.85 2.5 13.0167 2.5 13.2V14H8.75L10.75 16H0.5ZM14.05 16.4L10.6 12.95L12 11.55L14.05 13.6L19.1 8.55L20.5 9.95L14.05 16.4ZM8.5 8C7.4 8 6.45833 7.60833 5.675 6.825C4.89167 6.04167 4.5 5.1 4.5 4C4.5 2.9 4.89167 1.95833 5.675 1.175C6.45833 0.391667 7.4 0 8.5 0C9.6 0 10.5417 0.391667 11.325 1.175C12.1083 1.95833 12.5 2.9 12.5 4C12.5 5.1 12.1083 6.04167 11.325 6.825C10.5417 7.60833 9.6 8 8.5 8ZM8.5 6C9.05 6 9.52083 5.80417 9.9125 5.4125C10.3042 5.02083 10.5 4.55 10.5 4C10.5 3.45 10.3042 2.97917 9.9125 2.5875C9.52083 2.19583 9.05 2 8.5 2C7.95 2 7.47917 2.19583 7.0875 2.5875C6.69583 2.97917 6.5 3.45 6.5 4C6.5 4.55 6.69583 5.02083 7.0875 5.4125C7.47917 5.80417 7.95 6 8.5 6Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
3
client/public/icons/drawer/save.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.5 4V16C18.5 16.55 18.3042 17.0208 17.9125 17.4125C17.5208 17.8042 17.05 18 16.5 18H2.5C1.95 18 1.47917 17.8042 1.0875 17.4125C0.695833 17.0208 0.5 16.55 0.5 16V2C0.5 1.45 0.695833 0.979167 1.0875 0.5875C1.47917 0.195833 1.95 0 2.5 0H14.5L18.5 4ZM16.5 4.85L13.65 2H2.5V16H16.5V4.85ZM9.5 15C10.3333 15 11.0417 14.7083 11.625 14.125C12.2083 13.5417 12.5 12.8333 12.5 12C12.5 11.1667 12.2083 10.4583 11.625 9.875C11.0417 9.29167 10.3333 9 9.5 9C8.66667 9 7.95833 9.29167 7.375 9.875C6.79167 10.4583 6.5 11.1667 6.5 12C6.5 12.8333 6.79167 13.5417 7.375 14.125C7.95833 14.7083 8.66667 15 9.5 15ZM3.5 7H12.5V3H3.5V7Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 744 B |
3
client/public/icons/header/add_2.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.125 18V10H0.125V8H8.125V0H10.125V8H18.125V10H10.125V18H8.125Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 195 B |
3
client/public/icons/header/more.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 392 B |
3
client/public/icons/logo.svg
Normal 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 |
3
client/public/icons/sider/menu.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 12V10H18V12H0ZM0 7V5H18V7H0ZM0 2V0H18V2H0Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 176 B |
3
client/public/icons/sider/menu_open.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 12V10H13V12H0ZM16.6 11L11.6 6L16.6 1L18 2.4L14.4 6L18 9.6L16.6 11ZM0 7V5H10V7H0ZM0 2V0H13V2H0Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 228 B |
3
client/public/icons/sider/process-diagram.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.4 19L12 17.6L13.575 16L12 14.425L13.4 13L15 14.6L16.575 13L18 14.425L16.4 16L18 17.6L16.575 19L15 17.425L13.4 19ZM3 17C3.28333 17 3.52083 16.9042 3.7125 16.7125C3.90417 16.5208 4 16.2833 4 16C4 15.7167 3.90417 15.4792 3.7125 15.2875C3.52083 15.0958 3.28333 15 3 15C2.71667 15 2.47917 15.0958 2.2875 15.2875C2.09583 15.4792 2 15.7167 2 16C2 16.2833 2.09583 16.5208 2.2875 16.7125C2.47917 16.9042 2.71667 17 3 17ZM3 19C2.16667 19 1.45833 18.7083 0.875 18.125C0.291667 17.5417 0 16.8333 0 16C0 15.1667 0.291667 14.4583 0.875 13.875C1.45833 13.2917 2.16667 13 3 13C3.61667 13 4.17917 13.1708 4.6875 13.5125C5.19583 13.8542 5.56667 14.3167 5.8 14.9C6.45 14.7167 6.97917 14.3583 7.3875 13.825C7.79583 13.2917 8 12.6833 8 12V8C8 6.61667 8.4875 5.4375 9.4625 4.4625C10.4375 3.4875 11.6167 3 13 3H14.15L12.575 1.425L14 0L18 4L14 8L12.575 6.6L14.15 5H13C12.1667 5 11.4583 5.29167 10.875 5.875C10.2917 6.45833 10 7.16667 10 8V12C10 13.2167 9.60833 14.2875 8.825 15.2125C8.04167 16.1375 7.05 16.7083 5.85 16.925C5.65 17.5417 5.2875 18.0417 4.7625 18.425C4.2375 18.8083 3.65 19 3 19ZM1.4 7L0 5.6L1.575 4L0 2.425L1.4 1L3 2.6L4.575 1L6 2.425L4.4 4L6 5.6L4.575 7L3 5.425L1.4 7Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
client/public/icons/sider/running-processes.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13 16V14H9V9H7V11H0V5H7V7H9V2H13V0H20V6H13V4H11V12H13V10H20V16H13ZM15 14H18V12H15V14ZM2 9H5V7H2V9ZM15 4H18V2H15V4Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 246 B |
3
client/public/icons/sider/settings.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 18V12H10V14H18V16H10V18H8ZM0 16V14H6V16H0ZM4 12V10H0V8H4V6H6V12H4ZM8 10V8H18V10H8ZM12 6V0H14V2H18V4H14V6H12ZM0 4V2H10V4H0Z" fill="#606060"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 256 B |
@@ -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>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,7 +1,31 @@
|
|||||||
import React from 'react';
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import { useSetUserSelector } from './store/userStore';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
|
import ProtectedRoute from './pages/ProtectedRoute';
|
||||||
|
import MainLayout from './pages/MainLayout';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <div className="App"></div>;
|
const setUser = useSetUserSelector();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
if (storedUser) {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route path="*" element={<MainLayout />}></Route>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
130
client/src/api/api.ts
Normal 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;
|
175
client/src/components/ContentDrawer.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { Drawer } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Avatar, Typography } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
|
||||||
|
interface ContentDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
type: 'create' | 'edit';
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContentDrawer({
|
||||||
|
open,
|
||||||
|
closeDrawer,
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
login,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
}: ContentDrawerProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [width, setWidth] = useState<number | string>('30%');
|
||||||
|
|
||||||
|
const calculateWidths = () => {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const expanded = Math.max(windowWidth * 0.3, 300);
|
||||||
|
setWidth(expanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculateWidths();
|
||||||
|
window.addEventListener('resize', calculateWidths);
|
||||||
|
return () => window.removeEventListener('resize', calculateWidths);
|
||||||
|
}, []);
|
||||||
|
console.log(login, user?.login, login === user?.login);
|
||||||
|
|
||||||
|
const editDrawerTitle = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '24px',
|
||||||
|
width: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: '16px', width: '16px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
||||||
|
<Avatar
|
||||||
|
src={
|
||||||
|
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||||
|
}
|
||||||
|
size={40}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ display: 'block', fontSize: '20px' }}
|
||||||
|
>
|
||||||
|
{name} {login === user?.login ? t('you') : ''}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||||
|
{email}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '24px',
|
||||||
|
width: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: '18px', width: '16px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const createDrawerTitle = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={closeDrawer}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '24px',
|
||||||
|
width: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/arrow_back.svg"
|
||||||
|
alt="close_drawer"
|
||||||
|
style={{ height: '16px', width: '16px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
flex: 1,
|
||||||
|
fontSize: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('newAccount')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '24px',
|
||||||
|
width: '24px',
|
||||||
|
}}
|
||||||
|
onClick={closeDrawer}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/delete.svg"
|
||||||
|
alt="delete"
|
||||||
|
style={{ height: '18px', width: '16px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={type === 'create' ? createDrawerTitle : editDrawerTitle}
|
||||||
|
placement="right"
|
||||||
|
open={open}
|
||||||
|
width={width}
|
||||||
|
destroyOnHidden={true}
|
||||||
|
closable={false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
77
client/src/components/Header.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
import { Avatar } from 'antd';
|
||||||
|
import Title from 'antd/es/typography/Title';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import ContentDrawer from './ContentDrawer';
|
||||||
|
import UserEdit from './UserEdit';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
title: string;
|
||||||
|
additionalContent?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({ title, additionalContent }: HeaderProps) {
|
||||||
|
const [openEdit, setOpenEdit] = useState(false);
|
||||||
|
|
||||||
|
const showEditDrawer = () => setOpenEdit(true);
|
||||||
|
const closeEditDrawer = () => {
|
||||||
|
setOpenEdit(false);
|
||||||
|
};
|
||||||
|
const user = useUserSelector();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '72px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Title style={{ marginLeft: '16px' }} level={3}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '24px',
|
||||||
|
marginRight: '26px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{additionalContent}
|
||||||
|
<img
|
||||||
|
src="./icons/header/more.svg"
|
||||||
|
alt="more"
|
||||||
|
style={{ height: '16px', width: '16px' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: '2px solid #8BC34A',
|
||||||
|
height: '32px',
|
||||||
|
width: '32px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onClick={showEditDrawer}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size={25.77}
|
||||||
|
src={`https://gamma.heado.ru/go/ava?name=${user?.login}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ContentDrawer
|
||||||
|
login={user?.login}
|
||||||
|
name={user?.name}
|
||||||
|
email={user?.email}
|
||||||
|
open={openEdit}
|
||||||
|
closeDrawer={closeEditDrawer}
|
||||||
|
type="edit"
|
||||||
|
>
|
||||||
|
{user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
|
||||||
|
</ContentDrawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
122
client/src/components/SiderMenu.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
import { Divider, Menu, Tooltip } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface SiderMenuProps {
|
||||||
|
collapsed: boolean;
|
||||||
|
selectedKey: string;
|
||||||
|
hangleMenuClick: (e: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SiderMenu({
|
||||||
|
collapsed,
|
||||||
|
selectedKey,
|
||||||
|
hangleMenuClick,
|
||||||
|
}: SiderMenuProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const collapseStyle = collapsed
|
||||||
|
? { fontSize: '12px' }
|
||||||
|
: { fontSize: '12px', paddingLeft: '52px' };
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: 'toggle',
|
||||||
|
icon: (
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
!collapsed
|
||||||
|
? './icons/sider/menu_open.svg'
|
||||||
|
: './icons/sider/menu.svg'
|
||||||
|
}
|
||||||
|
alt="toggle"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: 'VORKOUT',
|
||||||
|
style: { fontSize: '20px' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '/process-diagram',
|
||||||
|
icon: (
|
||||||
|
<img src="./icons/sider/process-diagram.svg" alt="process diagram" />
|
||||||
|
),
|
||||||
|
label: (
|
||||||
|
<Tooltip title={t('processDiagrams')}>{t('processDiagrams')}</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '/running-processes',
|
||||||
|
icon: (
|
||||||
|
<img
|
||||||
|
src="./icons/sider/running-processes.svg"
|
||||||
|
alt="running processes"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: (
|
||||||
|
<Tooltip title={t('runningProcesses')}>{t('runningProcesses')}</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
!collapsed
|
||||||
|
? {
|
||||||
|
key: 'divider',
|
||||||
|
label: <Divider />,
|
||||||
|
style: {
|
||||||
|
marginBottom: '-16px',
|
||||||
|
marginTop: '-4px',
|
||||||
|
cursor: 'default',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
{
|
||||||
|
key: 'sub1',
|
||||||
|
icon: <img src="./icons/sider/settings.svg" alt="settings" />,
|
||||||
|
label: t('settings'),
|
||||||
|
className: 'no-expand-icon',
|
||||||
|
children: [
|
||||||
|
user && (user.role === 'OWNER' || user.role === 'ADMIN')
|
||||||
|
? {
|
||||||
|
key: '/accounts',
|
||||||
|
label: !collapsed ? (
|
||||||
|
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
|
||||||
|
) : (
|
||||||
|
t('accounts')
|
||||||
|
),
|
||||||
|
style: collapseStyle,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
{
|
||||||
|
key: '/events-list',
|
||||||
|
label: !collapsed ? (
|
||||||
|
<Tooltip title={t('eventsList')}>{t('eventsList')}</Tooltip>
|
||||||
|
) : (
|
||||||
|
t('eventsList')
|
||||||
|
),
|
||||||
|
style: collapseStyle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '/configuration',
|
||||||
|
label: !collapsed ? (
|
||||||
|
<Tooltip title={t('configuration')}>{t('configuration')}</Tooltip>
|
||||||
|
) : (
|
||||||
|
t('configuration')
|
||||||
|
),
|
||||||
|
style: collapseStyle,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
theme="light"
|
||||||
|
selectedKeys={[selectedKey]}
|
||||||
|
mode="inline"
|
||||||
|
onClick={hangleMenuClick}
|
||||||
|
defaultOpenKeys={['sub1']}
|
||||||
|
items={menuItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
262
client/src/components/UserCreate.tsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Upload,
|
||||||
|
Image,
|
||||||
|
UploadFile,
|
||||||
|
GetProp,
|
||||||
|
UploadProps,
|
||||||
|
message,
|
||||||
|
Spin,
|
||||||
|
} from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUserSelector } from "@/store/userStore";
|
||||||
|
import { UserCreate as NewUserCreate } from "@/types/user";
|
||||||
|
import { UserService } from "@/services/userService";
|
||||||
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
|
||||||
|
|
||||||
|
const getBase64 = (file: FileType): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UserCreateProps {
|
||||||
|
closeDrawer: () => void;
|
||||||
|
getUsers: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
|
||||||
|
const user = useUserSelector();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
|
const handlePreview = async (file: UploadFile) => {
|
||||||
|
if (!file.url && !file.preview) {
|
||||||
|
file.preview = await getBase64(file.originFileObj as FileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviewImage(file.url || (file.preview as string));
|
||||||
|
setPreviewOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
|
||||||
|
setFileList(newFileList);
|
||||||
|
|
||||||
|
const onFinish = async (values: NewUserCreate) => {
|
||||||
|
setLoading(true);
|
||||||
|
await UserService.createUser(values);
|
||||||
|
await getUsers();
|
||||||
|
closeDrawer();
|
||||||
|
setLoading(false);
|
||||||
|
message.info(t("createdAccountMessage"), 4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const customUploadButton = (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "102px",
|
||||||
|
width: "102px",
|
||||||
|
backgroundColor: "#E2E2E2",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: 8,
|
||||||
|
marginTop: 30,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./icons/drawer/add_photo_alternate.svg"
|
||||||
|
alt="add_photo_alternate"
|
||||||
|
style={{ height: "18px", width: "18px" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style={{ fontSize: "14px", color: "#8c8c8c" }}>
|
||||||
|
{t("selectPhoto")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const photoToUpload = (
|
||||||
|
<div style={{ height: "102px" }}>
|
||||||
|
<Upload
|
||||||
|
listType="picture-circle"
|
||||||
|
fileList={fileList}
|
||||||
|
onPreview={handlePreview}
|
||||||
|
onChange={handleChange}
|
||||||
|
beforeUpload={() => false}
|
||||||
|
>
|
||||||
|
{fileList.length > 0 ? null : customUploadButton}
|
||||||
|
</Upload>
|
||||||
|
{previewImage && (
|
||||||
|
<Image
|
||||||
|
wrapperStyle={{ display: "none" }}
|
||||||
|
preview={{
|
||||||
|
visible: previewOpen,
|
||||||
|
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||||
|
afterOpenChange: (visible) => !visible && setPreviewImage(""),
|
||||||
|
}}
|
||||||
|
src={previewImage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: "36px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{photoToUpload}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
name="user-edit-form"
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={{
|
||||||
|
name: "",
|
||||||
|
login: "",
|
||||||
|
password: "",
|
||||||
|
email: "",
|
||||||
|
bindTenantId: "",
|
||||||
|
role: "",
|
||||||
|
status: "",
|
||||||
|
}}
|
||||||
|
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t("name")}
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: t("nameMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("login")}
|
||||||
|
name="login"
|
||||||
|
rules={[{ required: true, message: t("loginMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("password")}
|
||||||
|
name="password"
|
||||||
|
rules={[{ message: t("passwordMessage") }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("email")}
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ message: t("emailMessage") },
|
||||||
|
{ type: "email", message: t("emailErrorMessage") },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("tenant")}
|
||||||
|
name="bindTenantId"
|
||||||
|
rules={[{ message: t("tenantMessage") }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("role")}
|
||||||
|
name="role"
|
||||||
|
rules={[{ required: true, message: t("roleMessage") }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t("roleMessage")}>
|
||||||
|
{user && user.role === "OWNER" ? (
|
||||||
|
<Option value="ADMIN">{t("ADMIN")}</Option>
|
||||||
|
) : undefined}
|
||||||
|
<Option value="EDITOR">{t("EDITOR")}</Option>
|
||||||
|
<Option value="VIEWER">{t("VIEWER")}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("status")}
|
||||||
|
name="status"
|
||||||
|
rules={[{ required: true, message: t("statusMessage") }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t("statusMessage")}>
|
||||||
|
<Option value="ACTIVE">{t("ACTIVE")}</Option>
|
||||||
|
<Option value="DISABLED">{t("DISABLED")}</Option>
|
||||||
|
<Option value="BLOCKED">{t("BLOCKED")}</Option>
|
||||||
|
<Option value="DELETED">{t("DELETED")}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
style={{ color: "#000" }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{" "}
|
||||||
|
{t("saving")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/reg.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: "18px", width: "18px" }}
|
||||||
|
/>{" "}
|
||||||
|
{t("addAccount")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
180
client/src/components/UserEdit.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { UserService } from '@/services/userService';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
import { UserUpdate } from '@/types/user';
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Form, Input, message, Select, Spin } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
interface UserEditProps {
|
||||||
|
userId?: number;
|
||||||
|
closeDrawer: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
|
||||||
|
const currentUser = useUserSelector();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [user, setUser] = useState<UserUpdate>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
login: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
bindTenantId: '',
|
||||||
|
role: 'VIEWER',
|
||||||
|
meta: {},
|
||||||
|
createdAt: '',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getUser() {
|
||||||
|
if (typeof userId === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = await UserService.getUserById(userId);
|
||||||
|
setUser(user);
|
||||||
|
form.setFieldsValue({ ...user });
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onFinish = async (values: UserUpdate) => {
|
||||||
|
setLoading(true);
|
||||||
|
let updatedUser: Partial<UserUpdate> = {};
|
||||||
|
|
||||||
|
(Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
|
||||||
|
if (values[key] !== user[key]) {
|
||||||
|
updatedUser = { ...updatedUser, [key]: values[key] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(updatedUser).length > 0) {
|
||||||
|
console.log('updateUser', userId, updatedUser);
|
||||||
|
await UserService.updateUser(userId!, updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
message.info(t('editAccountMessage'), 4);
|
||||||
|
closeDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="user-edit-form"
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={onFinish}
|
||||||
|
initialValues={{ ...user }}
|
||||||
|
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label={t('name')}
|
||||||
|
name="name"
|
||||||
|
rules={[{ message: t('nameMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
|
<Form.Item
|
||||||
|
label={t('login')}
|
||||||
|
name="login"
|
||||||
|
rules={[{ message: t('loginMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('password')}
|
||||||
|
name="password"
|
||||||
|
rules={[{ message: t('passwordMessage') }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('email')}
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: t('emailMessage') },
|
||||||
|
{ type: 'email', message: t('emailErrorMessage') },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('tenant')}
|
||||||
|
name="bindTenantId"
|
||||||
|
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{user?.id === currentUser?.id ? undefined : (
|
||||||
|
<Form.Item
|
||||||
|
label={t('role')}
|
||||||
|
name="role"
|
||||||
|
rules={[{ required: true, message: t('roleMessage') }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t('roleMessage')}>
|
||||||
|
{currentUser && currentUser.role === 'OWNER' ? (
|
||||||
|
<Option value="ADMIN">{t('ADMIN')}</Option>
|
||||||
|
) : undefined}
|
||||||
|
<Option value="EDITOR">{t('EDITOR')}</Option>
|
||||||
|
<Option value="VIEWER">{t('VIEWER')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t('status')}
|
||||||
|
name="status"
|
||||||
|
rules={[{ required: true, message: t('statusMessage') }]}
|
||||||
|
>
|
||||||
|
<Select placeholder={t('statusMessage')}>
|
||||||
|
<Option value="ACTIVE">{t('ACTIVE')}</Option>
|
||||||
|
<Option value="DISABLED">{t('DISABLED')}</Option>
|
||||||
|
<Option value="BLOCKED">{t('BLOCKED')}</Option>
|
||||||
|
<Option value="DELETED">{t('DELETED')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ flexGrow: 1 }} />
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
style={{ color: '#000' }}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} size="small"></Spin>{' '}
|
||||||
|
{t('saving')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src="/icons/drawer/save.svg"
|
||||||
|
alt="save"
|
||||||
|
style={{ height: '18px', width: '18px' }}
|
||||||
|
/>{' '}
|
||||||
|
{t('save')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
25
client/src/config/AppWrapper.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import '@/config/i18n';
|
||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { theme } from '@/config/customTheme';
|
||||||
|
|
||||||
|
import en from 'antd/locale/en_US';
|
||||||
|
import ru from 'antd/locale/ru_RU';
|
||||||
|
|
||||||
|
const antdLocales = {
|
||||||
|
en: en,
|
||||||
|
ru: ru,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AppWrapper({ children }: any) {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const currentLang = i18n.language.split('-')[0] as 'en' | 'ru';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider locale={antdLocales[currentLang]} theme={theme}>
|
||||||
|
<BrowserRouter>{children}</BrowserRouter>
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
15
client/src/config/customTheme.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const theme = {
|
||||||
|
token: {
|
||||||
|
fontFamily: 'Roboto, sans-serif',
|
||||||
|
colorPrimary: '#C2DA3D',
|
||||||
|
Menu: {
|
||||||
|
itemColor: 'f2f2f2',
|
||||||
|
itemBg: '#f2f2f2',
|
||||||
|
subMenuItemBg: '#f2f2f2',
|
||||||
|
iconSize: '18px',
|
||||||
|
},
|
||||||
|
Layout: {
|
||||||
|
bodyBg: '#f2f2f2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
102
client/src/config/i18n.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
supportedLngs: ['en', 'ru'],
|
||||||
|
interpolation: { escapeValue: false },
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: {
|
||||||
|
accounts: 'Accounts',
|
||||||
|
processDiagrams: 'Process diagrams',
|
||||||
|
runningProcesses: 'Running processes',
|
||||||
|
settings: 'Settings',
|
||||||
|
eventsList: 'Events list',
|
||||||
|
configuration: 'Configuration',
|
||||||
|
selectPhoto: 'Select photo',
|
||||||
|
name: 'Name',
|
||||||
|
login: 'Login',
|
||||||
|
password: 'Password',
|
||||||
|
email: 'Email',
|
||||||
|
tenant: 'Tenant',
|
||||||
|
role: 'Role',
|
||||||
|
status: 'Status',
|
||||||
|
nameMessage: 'Enter name',
|
||||||
|
loginMessage: 'Enter login',
|
||||||
|
passwordMessage: 'Enter password',
|
||||||
|
emailMessage: 'Enter email',
|
||||||
|
emailErrorMessage: 'Incorrect email',
|
||||||
|
tenantMessage: 'Enter tenant',
|
||||||
|
roleMessage: 'Choose role',
|
||||||
|
statusMessage: 'Choose status',
|
||||||
|
addAccount: 'Add account',
|
||||||
|
save: 'Save changes',
|
||||||
|
newAccount: 'New account',
|
||||||
|
ACTIVE: 'Active',
|
||||||
|
DISABLED: 'Disabled',
|
||||||
|
BLOCKED: 'Blocked',
|
||||||
|
DELETED: 'Deleted',
|
||||||
|
OWNER: 'Owner',
|
||||||
|
ADMIN: 'Admin',
|
||||||
|
EDITOR: 'Editor',
|
||||||
|
VIEWER: 'Viewer',
|
||||||
|
nameLogin: 'Name, login',
|
||||||
|
createdAt: 'Created',
|
||||||
|
saving: 'Saving...',
|
||||||
|
createdAccountMessage: 'User successfully created!',
|
||||||
|
editAccountMessage: 'User successfully updated!',
|
||||||
|
you: '(You)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
translation: {
|
||||||
|
accounts: 'Учетные записи',
|
||||||
|
processDiagrams: 'Схемы процессов',
|
||||||
|
runningProcesses: 'Запущенные процессы',
|
||||||
|
settings: 'Настройки',
|
||||||
|
eventsList: 'Справочкин событий',
|
||||||
|
configuration: 'Конфигурация',
|
||||||
|
selectPhoto: 'Выбрать фото',
|
||||||
|
name: 'Имя',
|
||||||
|
login: 'Логин',
|
||||||
|
password: 'Пароль',
|
||||||
|
email: 'Имейл',
|
||||||
|
tenant: 'Привязка',
|
||||||
|
role: 'Роль',
|
||||||
|
status: 'Статус',
|
||||||
|
nameMessage: 'Введите имя',
|
||||||
|
loginMessage: 'Введите логин',
|
||||||
|
passwordMessage: 'Введите пароль',
|
||||||
|
emailMessage: 'Введите имейл',
|
||||||
|
emailErrorMessage: 'Некорректный имейл',
|
||||||
|
tenantMessage: 'Введите привязку',
|
||||||
|
roleMessage: 'Выберите роль',
|
||||||
|
statusMessage: 'Выберите статус',
|
||||||
|
addAccount: 'Добавить аккаунт',
|
||||||
|
save: 'Сохранить изменения',
|
||||||
|
newAccount: 'Новая учетная запись',
|
||||||
|
ACTIVE: 'Активен',
|
||||||
|
DISABLED: 'Выключен',
|
||||||
|
BLOCKED: 'Заблокирован',
|
||||||
|
DELETED: 'Удален',
|
||||||
|
OWNER: 'Владелец',
|
||||||
|
ADMIN: 'Админ',
|
||||||
|
EDITOR: 'Редактор',
|
||||||
|
VIEWER: 'Наблюдатель',
|
||||||
|
nameLogin: 'Имя, Логин',
|
||||||
|
createdAt: 'Создано',
|
||||||
|
saving: 'Сохранение...',
|
||||||
|
createdAccountMessage: 'Пользователь успешно создан!',
|
||||||
|
editAccountMessage: 'Пользователь успешно обновлен!',
|
||||||
|
you: '(Вы)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
12
client/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_WEBSOCKET_PROTOCOL: string;
|
||||||
|
readonly VITE_APP_HTTP_PROTOCOL: string;
|
||||||
|
readonly VITE_APP_API_URL: string;
|
||||||
|
readonly VITE_APP_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
@@ -11,3 +11,34 @@ code {
|
|||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-menu-inline .ant-menu-submenu-selected {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-inline .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||||
|
color: #548d10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-inline .ant-menu-item-selected {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #548d10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item-selected {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item-selected a,
|
||||||
|
.ant-menu-item-selected {
|
||||||
|
color: #548d10 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-expand-icon .ant-menu-submenu-arrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
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 AppWrapper from '@/config/AppWrapper';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
);
|
);
|
||||||
root.render(<App />);
|
|
||||||
|
root.render(
|
||||||
|
<AppWrapper>
|
||||||
|
<App />
|
||||||
|
</AppWrapper>
|
||||||
|
);
|
220
client/src/pages/AccountsPage.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import ContentDrawer from "@/components/ContentDrawer";
|
||||||
|
import UserCreate from "@/components/UserCreate";
|
||||||
|
import { Avatar, Table } from "antd";
|
||||||
|
import { TableProps } from "antd/lib";
|
||||||
|
import { UserService } from "@/services/userService";
|
||||||
|
import UserEdit from "@/components/UserEdit";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function AccountsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [openCreate, setOpenCreate] = useState(false);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [activeAccount, setActiveAccount] = useState<
|
||||||
|
{ 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 (
|
||||||
|
<>
|
||||||
|
<Header
|
||||||
|
title={t("accounts")}
|
||||||
|
additionalContent={
|
||||||
|
<img
|
||||||
|
src="./icons/header/add_2.svg"
|
||||||
|
alt="add"
|
||||||
|
style={{
|
||||||
|
height: "18px",
|
||||||
|
width: "18px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
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={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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
client/src/pages/ConfigurationPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function ConfigurationPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title={t('configuration')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
11
client/src/pages/EventsListPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function EventsListPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title={t('eventsList')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
107
client/src/pages/LoginPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
87
client/src/pages/MainLayout.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Layout } from 'antd';
|
||||||
|
import Sider from 'antd/es/layout/Sider';
|
||||||
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import SiderMenu from '@/components/SiderMenu';
|
||||||
|
import ProcessDiagramPage from './ProcessDiagramPage';
|
||||||
|
import RunningProcessesPage from './RunningProcessesPage';
|
||||||
|
import AccountsPage from './AccountsPage';
|
||||||
|
import EventsListPage from './EventsListPage';
|
||||||
|
import ConfigurationPage from './ConfigurationPage';
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [selectedKey, setSelectedKey] = useState('1');
|
||||||
|
|
||||||
|
const [width, setWidth] = useState<number | string>('15%');
|
||||||
|
const [collapsedWidth, setCollapsedWidth] = useState(50);
|
||||||
|
|
||||||
|
const calculateWidths = () => {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const expanded = Math.min(Math.max(windowWidth * 0.15, 180), 240);
|
||||||
|
const collapsed = Math.max(windowWidth * 0.038, 50);
|
||||||
|
setWidth(expanded);
|
||||||
|
setCollapsedWidth(collapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculateWidths();
|
||||||
|
window.addEventListener('resize', calculateWidths);
|
||||||
|
return () => window.removeEventListener('resize', calculateWidths);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname === '/') {
|
||||||
|
navigate('/process-diagram');
|
||||||
|
}
|
||||||
|
setSelectedKey(location.pathname);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
function hangleMenuClick(e: any) {
|
||||||
|
const key = e.key;
|
||||||
|
if (key === 'toggle') {
|
||||||
|
setCollapsed(!collapsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'divider') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedKey(key);
|
||||||
|
navigate(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
|
<Sider
|
||||||
|
className="sider"
|
||||||
|
collapsible
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={(value) => setCollapsed(value)}
|
||||||
|
theme="light"
|
||||||
|
width={width}
|
||||||
|
collapsedWidth={collapsedWidth}
|
||||||
|
trigger={null}
|
||||||
|
>
|
||||||
|
<SiderMenu
|
||||||
|
collapsed={collapsed}
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
hangleMenuClick={hangleMenuClick}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/process-diagram" element={<ProcessDiagramPage />} />
|
||||||
|
<Route path="/running-processes" element={<RunningProcessesPage />} />
|
||||||
|
<Route path="/accounts" element={<AccountsPage />} />
|
||||||
|
<Route path="/events-list" element={<EventsListPage />} />
|
||||||
|
<Route path="/configuration" element={<ConfigurationPage />} />
|
||||||
|
<Route path="*" element={<div>404 Not Found</div>} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
11
client/src/pages/ProcessDiagramPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function ProcessDiagramPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title={t('processDiagrams')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
18
client/src/pages/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useUserSelector } from '@/store/userStore';
|
||||||
|
|
||||||
|
const ProtectedRoute = (): React.JSX.Element => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const user = useUserSelector();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user?.id) {
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
|
export default ProtectedRoute;
|
11
client/src/pages/RunningProcessesPage.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Header from '@/components/Header';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function RunningProcessesPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header title={t('runningProcesses')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
30
client/src/services/authService.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import api from "@/api/api";
|
||||||
|
import { useAuthStore } from "@/store/authStore";
|
||||||
|
import { Auth } from "@/types/auth";
|
||||||
|
import { UserService } from "./userService";
|
||||||
|
import { useUserStore } from "@/store/userStore";
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
static async login(auth: Auth) {
|
||||||
|
const token = await api.login(auth);
|
||||||
|
useAuthStore.getState().setAccessToken(token.accessToken);
|
||||||
|
localStorage.setItem('refreshToken', token.refreshToken as string);
|
||||||
|
await UserService.getProfile().then((user) => {
|
||||||
|
useUserStore.getState().setUser(user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async logout() {
|
||||||
|
console.log('logout');
|
||||||
|
useUserStore.getState().setUser(null);
|
||||||
|
useAuthStore.getState().setAccessToken(null);
|
||||||
|
localStorage.removeItem('userInfo');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
static async refresh() {
|
||||||
|
console.log('refresh');
|
||||||
|
const token = await api.refreshToken();
|
||||||
|
useAuthStore.getState().setAccessToken(token.accessToken);
|
||||||
|
}
|
||||||
|
}
|
38
client/src/services/userService.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import api from '@/api/api';
|
||||||
|
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
static async getProfile(): Promise<User> {
|
||||||
|
console.log('getProfile');
|
||||||
|
const user = api.getProfile();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUsers(
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<AllUserResponse> {
|
||||||
|
console.log('getUsers');
|
||||||
|
const allUsers = api.getUsers(page, limit);
|
||||||
|
return allUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUserById(userId: number): Promise<User> {
|
||||||
|
console.log('getUserById');
|
||||||
|
const user = api.getUserById(userId);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createUser(user: UserCreate): Promise<User> {
|
||||||
|
console.log('createUser');
|
||||||
|
const createdUser = api.createUser(user);
|
||||||
|
return createdUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||||
|
console.log('updateUser');
|
||||||
|
const updatedUser = api.updateUser(userId, user);
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
}
|
18
client/src/store/authStore.ts
Normal file
@@ -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);
|
||||||
|
};
|
36
client/src/store/userStore.ts
Normal 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
@@ -0,0 +1,4 @@
|
|||||||
|
import { components } from './openapi-types';
|
||||||
|
|
||||||
|
export type Auth = components['schemas']['Auth'];
|
||||||
|
export type Tokens = components['schemas']['Tokens'];
|
719
client/src/types/openapi-types.ts
Normal 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
@@ -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'];
|
@@ -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": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
23
client/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'build',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
port: 3000,
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@@ -3,13 +3,13 @@ version: "3.1"
|
|||||||
services:
|
services:
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:4-management-alpine
|
image: rabbitmq:4-management-alpine
|
||||||
container_name: rabbitmq
|
container_name: rabbitmq-connect
|
||||||
ports:
|
ports:
|
||||||
- 5672:5672
|
- 5672:5672
|
||||||
- 15672:15672
|
- 15672:15672
|
||||||
db:
|
db:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: mysql
|
container_name: mysql-connect
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: hackme
|
MYSQL_ROOT_PASSWORD: hackme
|
||||||
MYSQL_DATABASE: connect_test
|
MYSQL_DATABASE: connect_test
|
||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: redis
|
container_name: redis-connect
|
||||||
command: redis-server --requirepass password
|
command: redis-server --requirepass password
|
||||||
environment:
|
environment:
|
||||||
REDIS_PASSWORD: hackme
|
REDIS_PASSWORD: hackme
|
||||||
|
@@ -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
|
||||||
|