VORKOUT-8 #13
18
Makefile
18
Makefile
@ -12,8 +12,7 @@ services:
|
||||
|
||||
start-api:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
python -m ${API_APPLICATION_NAME}
|
||||
poetry run python -m ${API_APPLICATION_NAME}
|
||||
|
||||
start-client:
|
||||
cd client && \
|
||||
@ -21,25 +20,21 @@ start-client:
|
||||
|
||||
migrate:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args)
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic upgrade $(args)
|
||||
|
||||
downgrade:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' alembic downgrade -1
|
||||
PYTHONPATH='../..' poetry run alembic downgrade -1
|
||||
|
||||
revision:
|
||||
cd api && \
|
||||
source .venv/bin/activate && \
|
||||
cd $(API_APPLICATION_NAME)/db && \
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate
|
||||
PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True poetry run alembic revision --autogenerate
|
||||
|
||||
venv-api:
|
||||
cd api && \
|
||||
poetry env activate \
|
||||
poetry install
|
||||
|
||||
venv-client:
|
||||
@ -62,3 +57,8 @@ format-api:
|
||||
check-api:
|
||||
cd api && \
|
||||
poetry run ruff format . --check
|
||||
|
||||
regenerate-openapi-local:
|
||||
cd client \
|
||||
rm src/types/openapi-types.ts \
|
||||
npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts
|
||||
|
38
api/api/db/alembic/versions/93106fbe7d83_.py
Normal file
38
api/api/db/alembic/versions/93106fbe7d83_.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 93106fbe7d83
|
||||
Revises: f1b06efacec0
|
||||
Create Date: 2025-06-26 16:36:02.270706
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '93106fbe7d83'
|
||||
down_revision: Union[str, None] = 'f1b06efacec0'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('account_keyring', 'key_value',
|
||||
existing_type=mysql.VARCHAR(length=255),
|
||||
type_=sa.String(length=512),
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('account_keyring', 'key_value',
|
||||
existing_type=sa.String(length=512),
|
||||
type_=mysql.VARCHAR(length=255),
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
@ -1,19 +1,17 @@
|
||||
from typing import Optional
|
||||
import math
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import insert, select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import func, insert, select
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.tables.account import account_table
|
||||
|
||||
from api.schemas.account.account import User
|
||||
from api.schemas.endpoints.account import AllUserResponse, all_user_adapter
|
||||
from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate
|
||||
|
||||
|
||||
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[User]:
|
||||
async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]:
|
||||
"""
|
||||
Получает список ползовелей заданных значениями page, limit.
|
||||
"""
|
||||
@ -47,31 +45,28 @@ async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Opt
|
||||
|
||||
validated_users = all_user_adapter.validate_python(users_data)
|
||||
|
||||
return AllUserResponse(users=validated_users, amount_count=total_count, amount_pages=total_pages)
|
||||
return AllUserResponse(
|
||||
users=validated_users,
|
||||
amount_count=total_count,
|
||||
amount_pages=total_pages,
|
||||
current_page=page,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
async def get_user_by_id(connection: AsyncConnection, id: int) -> Optional[User]:
|
||||
async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]:
|
||||
"""
|
||||
Получает юзера по id.
|
||||
"""
|
||||
query = select(account_table).where(account_table.c.id == id)
|
||||
query = select(account_table).where(account_table.c.id == user_id)
|
||||
|
||||
user_db_cursor = await connection.execute(query)
|
||||
user_db = user_db_cursor.one_or_none()
|
||||
user = user_db_cursor.mappings().one_or_none()
|
||||
|
||||
if not user_db:
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user_data = {
|
||||
column.name: (
|
||||
getattr(user_db, column.name).name
|
||||
if isinstance(getattr(user_db, column.name), Enum)
|
||||
else getattr(user_db, column.name)
|
||||
)
|
||||
for column in account_table.columns
|
||||
}
|
||||
|
||||
return User.model_validate(user_data)
|
||||
return AllUser.model_validate(user)
|
||||
|
||||
|
||||
async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||
@ -107,7 +102,7 @@ async def update_user_by_id(connection: AsyncConnection, update_values, user) ->
|
||||
await connection.commit()
|
||||
|
||||
|
||||
async def create_user(connection: AsyncConnection, user: User, creator_id: int) -> Optional[User]:
|
||||
async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]:
|
||||
"""
|
||||
Создает нове поле в таблице account_table.
|
||||
"""
|
||||
@ -117,14 +112,15 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int)
|
||||
email=user.email,
|
||||
bind_tenant_id=user.bind_tenant_id,
|
||||
role=user.role.value,
|
||||
meta=user.meta,
|
||||
meta=user.meta or {},
|
||||
creator_id=creator_id,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
status=user.status.value,
|
||||
)
|
||||
|
||||
await connection.execute(query)
|
||||
res = await connection.execute(query)
|
||||
|
||||
await connection.commit()
|
||||
new_user = await get_user_by_id(connection, res.lastrowid)
|
||||
|
||||
return user
|
||||
return new_user
|
||||
|
@ -8,13 +8,14 @@ from api.db.tables.account import account_table, account_keyring_table, KeyType,
|
||||
|
||||
from api.schemas.account.account import User
|
||||
from api.schemas.account.account_keyring import AccountKeyring
|
||||
from api.schemas.endpoints.account import AllUser
|
||||
|
||||
from api.utils.key_id_gen import KeyIdGenerator
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||
async def get_user(connection: AsyncConnection, login: str) -> tuple[Optional[AllUser], Optional[AccountKeyring]]:
|
||||
query = (
|
||||
select(account_table, account_keyring_table)
|
||||
.join(account_keyring_table, account_table.c.id == account_keyring_table.c.owner_id)
|
||||
@ -45,7 +46,7 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]:
|
||||
for column in account_keyring_table.columns
|
||||
}
|
||||
|
||||
user = User.model_validate(user_data)
|
||||
user = AllUser.model_validate(user_data)
|
||||
password = AccountKeyring.model_validate(password_data)
|
||||
return user, password
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
from typing import Optional
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import insert, select
|
||||
from sqlalchemy import insert, select, update
|
||||
from sqlalchemy.dialects.mysql import insert as mysql_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.tables.account import account_keyring_table
|
||||
|
||||
from api.db.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]:
|
||||
@ -67,3 +68,37 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i
|
||||
await connection.commit()
|
||||
|
||||
return key
|
||||
|
||||
|
||||
async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int):
|
||||
if password is None:
|
||||
password = hasher.generate_password()
|
||||
hashed_password = hasher.hash_data(password)
|
||||
stmt = mysql_insert(account_keyring_table).values(
|
||||
owner_id=owner_id,
|
||||
key_type=KeyType.PASSWORD.value,
|
||||
|
||||
key_id="PASSWORD",
|
||||
key_value=hashed_password,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
expiry=datetime.now(timezone.utc) + timedelta(days=365),
|
||||
status=KeyStatus.ACTIVE,
|
||||
)
|
||||
stmt.on_duplicate_key_update(key_value=hashed_password)
|
||||
await connection.execute(stmt)
|
||||
await connection.commit()
|
||||
|
||||
|
||||
async def update_password_key(connection: AsyncConnection, owner_id: int, password: str):
|
||||
stmt = select(account_keyring_table).where(account_keyring_table.c.owner_id == owner_id)
|
||||
result = await connection.execute(stmt)
|
||||
keyring = result.one_or_none()
|
||||
if not keyring:
|
||||
await create_password_key(connection, password, owner_id)
|
||||
else:
|
||||
stmt = (
|
||||
update(account_keyring_table)
|
||||
.values(key_value=hasher.hash_data(password), expiry=datetime.now(timezone.utc) + timedelta(days=365))
|
||||
.where(account_keyring_table.c.owner_id == owner_id)
|
||||
)
|
||||
await connection.execute(stmt)
|
||||
await connection.commit()
|
||||
|
@ -60,7 +60,7 @@ account_keyring_table = Table(
|
||||
Column("owner_id", UnsignedInt, ForeignKey("account.id"), primary_key=True, nullable=False),
|
||||
Column("key_type", SQLAEnum(KeyType), primary_key=True, nullable=False),
|
||||
Column("key_id", String(40), primary_key=True, default=None),
|
||||
Column("key_value", String(255), nullable=False),
|
||||
Column("key_value", String(512), nullable=False),
|
||||
Column("created_at", DateTime(timezone=True), server_default=func.now()),
|
||||
Column("expiry", DateTime(timezone=True), nullable=True),
|
||||
Column("status", SQLAEnum(KeyStatus), nullable=False),
|
||||
|
@ -4,29 +4,24 @@ from fastapi import (
|
||||
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,
|
||||
update_user_by_id,
|
||||
create_user,
|
||||
get_user_by_login,
|
||||
get_user_accaunt_page,
|
||||
get_user_by_id,
|
||||
get_user_by_login,
|
||||
update_user_by_id,
|
||||
)
|
||||
|
||||
from api.schemas.account.account import User
|
||||
from api.db.logic.keyring import create_password_key, update_password_key
|
||||
from api.db.tables.account import AccountStatus
|
||||
from api.schemas.account.account import User
|
||||
from api.schemas.base import bearer_schema
|
||||
from api.schemas.endpoints.account import UserUpdate, AllUserResponse
|
||||
from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate
|
||||
from api.services.auth import get_current_user
|
||||
|
||||
from api.services.user_role_validation import db_user_role_validation
|
||||
from api.services.update_data_validation import update_user_data_changes
|
||||
|
||||
from api.services.user_role_validation import db_user_role_validation
|
||||
|
||||
api_router = APIRouter(
|
||||
prefix="/account",
|
||||
@ -51,9 +46,11 @@ async def get_all_account(
|
||||
return user_list
|
||||
|
||||
|
||||
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
@api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
|
||||
async def get_account(
|
||||
user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
|
||||
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)
|
||||
|
||||
@ -65,26 +62,27 @@ async def get_account(
|
||||
return user
|
||||
|
||||
|
||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser)
|
||||
async def create_account(
|
||||
user: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
|
||||
user: UserCreate,
|
||||
connection: AsyncConnection = Depends(get_connection_dep),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
authorize_user = await db_user_role_validation(connection, current_user)
|
||||
|
||||
user_validation = await get_user_by_login(connection, user.login)
|
||||
|
||||
if user_validation is None:
|
||||
await create_user(connection, user, authorize_user.id)
|
||||
user_new = await get_user_by_login(connection, user.login)
|
||||
return user_new
|
||||
|
||||
new_user = await create_user(connection, user, authorize_user.id)
|
||||
await create_password_key(connection, user.password, new_user.id)
|
||||
return new_user
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists."
|
||||
)
|
||||
|
||||
|
||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
@api_router.put("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=UserUpdate)
|
||||
async def update_account(
|
||||
user_id: int,
|
||||
user_update: UserUpdate,
|
||||
@ -97,12 +95,15 @@ async def update_account(
|
||||
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)
|
||||
|
||||
update_values = update_user_data_changes(user_update, user)
|
||||
|
||||
if update_values is None:
|
||||
return user
|
||||
|
||||
user_update_data = User.model_validate({**user.model_dump(), **update_values})
|
||||
user_update_data = UserUpdate.model_validate({**user.model_dump(), **update_values})
|
||||
|
||||
await update_user_by_id(connection, update_values, user)
|
||||
|
||||
@ -113,7 +114,9 @@ async def update_account(
|
||||
|
||||
@api_router.delete("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User)
|
||||
async def delete_account(
|
||||
user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user)
|
||||
user_id: int,
|
||||
connection: AsyncConnection = Depends(get_connection_dep),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
authorize_user = await db_user_role_validation(connection, current_user)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import EmailStr, Field, TypeAdapter
|
||||
|
||||
from api.db.tables.account import AccountRole, AccountStatus
|
||||
|
||||
from api.schemas.base import Base
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ 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
|
||||
@ -20,6 +21,17 @@ class UserUpdate(Base):
|
||||
status: Optional[AccountStatus] = None
|
||||
|
||||
|
||||
class UserCreate(Base):
|
||||
cyrussmeat
commented
Валидация идёт на уровне этой модели или на другом дескрипторе? Просто тут name, login, role, status не должны быть Optional. Валидация идёт на уровне этой модели или на другом дескрипторе?
Просто тут name, login, role, status не должны быть Optional.
vlad.dev
commented
На уровне этой, поправлю на обязательные На уровне этой, поправлю на обязательные
|
||||
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
|
||||
@ -35,6 +47,8 @@ class AllUserResponse(Base):
|
||||
users: List[AllUser]
|
||||
amount_count: int
|
||||
amount_pages: int
|
||||
current_page: int
|
||||
limit: int
|
||||
|
||||
|
||||
all_user_adapter = TypeAdapter(List[AllUser])
|
||||
|
@ -1,27 +1,25 @@
|
||||
from fastapi import Request, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection
|
||||
|
||||
from api.db.logic.auth import get_user
|
||||
|
||||
# # from backend.schemas.users.token import TokenData
|
||||
from api.schemas.account.account import User
|
||||
from api.db.tables.account import AccountStatus
|
||||
|
||||
from api.utils.hasher import Hasher
|
||||
from api.schemas.endpoints.account import AllUser
|
||||
from api.utils.hasher import hasher
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> Optional[User]:
|
||||
async def get_current_user(request: Request) -> str | HTTPException:
|
||||
if not hasattr(request.state, "current_user"):
|
||||
return HTTPException(status_code=401, detail="Unauthorized")
|
||||
return request.state.current_user
|
||||
|
||||
|
||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[User]:
|
||||
async def authenticate_user(connection: AsyncConnection, username: str, password: str) -> Optional[AllUser]:
|
||||
sql_user, sql_password = await get_user(connection, username)
|
||||
|
||||
if not sql_user or sql_user.status != AccountStatus.ACTIVE:
|
||||
return None
|
||||
hasher = Hasher()
|
||||
if not hasher.verify_data(password, sql_password.key_value):
|
||||
return None
|
||||
return sql_user
|
||||
|
@ -1,4 +1,6 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
|
||||
# Хешер для работы с паролем.
|
||||
|
||||
@ -14,3 +16,10 @@ class Hasher:
|
||||
def verify_data(self, password: str, hashed: str) -> bool:
|
||||
# Проверяет пароль путем сравнения его хеша с сохраненным хешем.
|
||||
return self.hash_data(password) == hashed
|
||||
|
||||
@staticmethod
|
||||
def generate_password() -> str:
|
||||
return secrets.token_urlsafe(20)
|
||||
|
||||
|
||||
hasher = Hasher()
|
||||
|
@ -1,32 +1,23 @@
|
||||
import os
|
||||
import asyncio
|
||||
import hashlib
|
||||
import secrets
|
||||
import os
|
||||
|
||||
from api.db.connection.session import get_connection
|
||||
from api.db.tables.account import account_table, account_keyring_table, AccountRole, KeyType, KeyStatus
|
||||
from api.db.tables.account import account_keyring_table, account_table, AccountRole, KeyStatus, KeyType
|
||||
from api.utils.hasher import hasher
|
||||
from api.utils.key_id_gen import KeyIdGenerator
|
||||
|
||||
INIT_LOCK_FILE = "../init.lock"
|
||||
DEFAULT_LOGIN = "vorkout"
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
|
||||
def generate_password() -> str:
|
||||
return secrets.token_urlsafe(20)
|
||||
|
||||
|
||||
async def init():
|
||||
if os.path.exists(INIT_LOCK_FILE):
|
||||
print("Sorry, service is already initialized")
|
||||
return
|
||||
|
||||
async with get_connection() as conn:
|
||||
password = generate_password()
|
||||
hashed_password = hash_password(password)
|
||||
password = hasher.generate_password()
|
||||
hashed_password = hasher.hash_data(password)
|
||||
|
||||
create_user_query = account_table.insert().values(
|
||||
name=DEFAULT_LOGIN,
|
||||
|
@ -1,10 +1,9 @@
|
||||
import axios from 'axios';
|
||||
// import { Auth, Tokens } from '../types/auth';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import { Auth, Tokens } from '@/types/auth';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { AuthService } from '@/services/authService';
|
||||
import { User } from '@/types/user';
|
||||
import { User, UserCreate, UserUpdate } from '@/types/user';
|
||||
|
||||
const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${
|
||||
import.meta.env.VITE_APP_API_URL
|
||||
@ -109,6 +108,23 @@ const api = {
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getUserById(userId: number): Promise<User> {
|
||||
const response = await base.get<User>(`/account/${userId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createUser(user: UserCreate): Promise<User> {
|
||||
const response = await base.post<User>('/account', user);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||
const response = await base.put<User>(`/account/${userId}`, user);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// keyrings
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
@ -2,12 +2,16 @@ import { Drawer } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Avatar, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
|
||||
interface ContentDrawerProps {
|
||||
open: boolean;
|
||||
closeDrawer: () => void;
|
||||
children: React.ReactNode;
|
||||
type: 'create' | 'edit';
|
||||
login?: string;
|
||||
name?: string;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export default function ContentDrawer({
|
||||
@ -15,7 +19,11 @@ export default function ContentDrawer({
|
||||
closeDrawer,
|
||||
children,
|
||||
type,
|
||||
login,
|
||||
name,
|
||||
email,
|
||||
}: ContentDrawerProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const [width, setWidth] = useState<number | string>('30%');
|
||||
|
||||
@ -30,6 +38,7 @@ export default function ContentDrawer({
|
||||
window.addEventListener('resize', calculateWidths);
|
||||
return () => window.removeEventListener('resize', calculateWidths);
|
||||
}, []);
|
||||
console.log(login, user?.login, login === user?.login);
|
||||
|
||||
const editDrawerTitle = (
|
||||
<div
|
||||
@ -59,16 +68,21 @@ export default function ContentDrawer({
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1 }}>
|
||||
<Avatar
|
||||
src="https://cdn-icons-png.flaticon.com/512/219/219986.png"
|
||||
src={
|
||||
login ? `https://gamma.heado.ru/go/ava?name=${login}` : undefined
|
||||
}
|
||||
size={40}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text strong style={{ display: 'block' }}>
|
||||
Александр Александров
|
||||
<Typography.Text
|
||||
strong
|
||||
style={{ display: 'block', fontSize: '20px' }}
|
||||
>
|
||||
{name} {login === user?.login ? t('you') : ''}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 14 }}>
|
||||
alexandralex@vorkout.ru
|
||||
{email}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,7 +166,7 @@ export default function ContentDrawer({
|
||||
placement="right"
|
||||
open={open}
|
||||
width={width}
|
||||
destroyOnClose={true}
|
||||
destroyOnHidden={true}
|
||||
closable={false}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { Avatar } from 'antd';
|
||||
import Title from 'antd/es/typography/Title';
|
||||
import { useState } from 'react';
|
||||
import ContentDrawer from './ContentDrawer';
|
||||
import UserEdit from './UserEdit';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
@ -7,6 +11,13 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export default function Header({ title, additionalContent }: HeaderProps) {
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
|
||||
const showEditDrawer = () => setOpenEdit(true);
|
||||
const closeEditDrawer = () => {
|
||||
setOpenEdit(false);
|
||||
};
|
||||
const user = useUserSelector();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -43,13 +54,24 @@ export default function Header({ title, additionalContent }: HeaderProps) {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={showEditDrawer}
|
||||
>
|
||||
<Avatar
|
||||
size={25.77}
|
||||
src={`https://cdn-icons-png.flaticon.com/512/219/219986.png`}
|
||||
src={`https://gamma.heado.ru/go/ava?name=${user?.login}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContentDrawer
|
||||
login={user?.login}
|
||||
name={user?.name}
|
||||
email={user?.email}
|
||||
open={openEdit}
|
||||
closeDrawer={closeEditDrawer}
|
||||
type="edit"
|
||||
>
|
||||
{user?.id && <UserEdit closeDrawer={closeEditDrawer} userId={user?.id} />}
|
||||
</ContentDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { Divider, Menu, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -13,6 +14,7 @@ export default function SiderMenu({
|
||||
selectedKey,
|
||||
hangleMenuClick,
|
||||
}: SiderMenuProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const collapseStyle = collapsed
|
||||
? { fontSize: '12px' }
|
||||
@ -74,15 +76,17 @@ export default function SiderMenu({
|
||||
label: t('settings'),
|
||||
className: 'no-expand-icon',
|
||||
children: [
|
||||
{
|
||||
key: '/accounts',
|
||||
label: !collapsed ? (
|
||||
<Tooltip title={t('accounts')}>{t('accounts')}</Tooltip>
|
||||
) : (
|
||||
t('accounts')
|
||||
),
|
||||
style: collapseStyle,
|
||||
},
|
||||
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 ? (
|
||||
|
@ -8,13 +8,19 @@ import {
|
||||
UploadFile,
|
||||
GetProp,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
message,
|
||||
Spin,
|
||||
} from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUserSelector } from "@/store/userStore";
|
||||
import { UserCreate as NewUserCreate } from "@/types/user";
|
||||
import { UserService } from "@/services/userService";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
type FileType = Parameters<GetProp<UploadProps, "beforeUpload">>[0];
|
||||
|
||||
const getBase64 = (file: FileType): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
@ -24,10 +30,17 @@ const getBase64 = (file: FileType): Promise<string> =>
|
||||
reader.onerror = (error) => reject(error);
|
||||
});
|
||||
|
||||
export default function UserCreate() {
|
||||
interface UserCreateProps {
|
||||
closeDrawer: () => void;
|
||||
getUsers: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) {
|
||||
const user = useUserSelector();
|
||||
const { t } = useTranslation();
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState('');
|
||||
const [previewImage, setPreviewImage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
@ -40,40 +53,49 @@ export default function UserCreate() {
|
||||
setPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) =>
|
||||
const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) =>
|
||||
setFileList(newFileList);
|
||||
|
||||
const onFinish = async (values: NewUserCreate) => {
|
||||
setLoading(true);
|
||||
await UserService.createUser(values);
|
||||
await getUsers();
|
||||
closeDrawer();
|
||||
setLoading(false);
|
||||
message.info(t("createdAccountMessage"), 4);
|
||||
};
|
||||
|
||||
const customUploadButton = (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
height: '102px',
|
||||
width: '102px',
|
||||
backgroundColor: '#E2E2E2',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: "102px",
|
||||
width: "102px",
|
||||
backgroundColor: "#E2E2E2",
|
||||
borderRadius: "50%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: 8,
|
||||
marginTop: 30,
|
||||
cursor: 'pointer',
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="./icons/drawer/add_photo_alternate.svg"
|
||||
alt="add_photo_alternate"
|
||||
style={{ height: '18px', width: '18px' }}
|
||||
style={{ height: "18px", width: "18px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span style={{ fontSize: '14px', color: '#8c8c8c' }}>
|
||||
{t('selectPhoto')}
|
||||
<span style={{ fontSize: "14px", color: "#8c8c8c" }}>
|
||||
{t("selectPhoto")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const photoToUpload = (
|
||||
<div style={{ height: '102px' }}>
|
||||
<div style={{ height: "102px" }}>
|
||||
<Upload
|
||||
listType="picture-circle"
|
||||
fileList={fileList}
|
||||
@ -85,11 +107,11 @@ export default function UserCreate() {
|
||||
</Upload>
|
||||
{previewImage && (
|
||||
<Image
|
||||
wrapperStyle={{ display: 'none' }}
|
||||
wrapperStyle={{ display: "none" }}
|
||||
preview={{
|
||||
visible: previewOpen,
|
||||
onVisibleChange: (visible) => setPreviewOpen(visible),
|
||||
afterOpenChange: (visible) => !visible && setPreviewImage(''),
|
||||
afterOpenChange: (visible) => !visible && setPreviewImage(""),
|
||||
}}
|
||||
src={previewImage}
|
||||
/>
|
||||
@ -100,24 +122,24 @@ export default function UserCreate() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '36px',
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: "36px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{photoToUpload}
|
||||
@ -126,83 +148,85 @@ export default function UserCreate() {
|
||||
<Form
|
||||
name="user-edit-form"
|
||||
layout="vertical"
|
||||
// onFinish={onFinish}
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
name: '',
|
||||
login: '',
|
||||
password: '',
|
||||
email: '',
|
||||
tenant: '',
|
||||
role: '',
|
||||
status: '',
|
||||
name: "",
|
||||
login: "",
|
||||
password: "",
|
||||
email: "",
|
||||
bindTenantId: "",
|
||||
role: "",
|
||||
status: "",
|
||||
}}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
style={{ flex: 1, display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('name')}
|
||||
label={t("name")}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t('nameMessage') }]}
|
||||
rules={[{ required: true, message: t("nameMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('login')}
|
||||
label={t("login")}
|
||||
name="login"
|
||||
rules={[{ required: true, message: t('loginMessage') }]}
|
||||
rules={[{ required: true, message: t("loginMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('password')}
|
||||
label={t("password")}
|
||||
name="password"
|
||||
rules={[{ required: true, message: t('passwordMessage') }]}
|
||||
rules={[{ message: t("passwordMessage") }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('email')}
|
||||
label={t("email")}
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: t('emailMessage') },
|
||||
{ type: 'email', message: t('emailErrorMessage') },
|
||||
{ message: t("emailMessage") },
|
||||
{ type: "email", message: t("emailErrorMessage") },
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('tenant')}
|
||||
name="tenant"
|
||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||
label={t("tenant")}
|
||||
name="bindTenantId"
|
||||
rules={[{ message: t("tenantMessage") }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('role')}
|
||||
label={t("role")}
|
||||
name="role"
|
||||
rules={[{ required: true, message: t('roleMessage') }]}
|
||||
rules={[{ required: true, message: t("roleMessage") }]}
|
||||
>
|
||||
<Select placeholder={t('roleMessage')}>
|
||||
<Option value="Директор магазина">Директор магазина</Option>
|
||||
<Option value="Менеджер">Менеджер</Option>
|
||||
<Option value="Кассир">Кассир</Option>
|
||||
<Select placeholder={t("roleMessage")}>
|
||||
{user && user.role === "OWNER" ? (
|
||||
<Option value="ADMIN">{t("ADMIN")}</Option>
|
||||
) : undefined}
|
||||
<Option value="EDITOR">{t("EDITOR")}</Option>
|
||||
<Option value="VIEWER">{t("VIEWER")}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('status')}
|
||||
label={t("status")}
|
||||
name="status"
|
||||
rules={[{ required: true, message: t('statusMessage') }]}
|
||||
rules={[{ required: true, message: t("statusMessage") }]}
|
||||
>
|
||||
<Select placeholder={t('statusMessage')}>
|
||||
<Option value="ACTIVE">Активен</Option>
|
||||
<Option value="DISABLED">Неактивен</Option>
|
||||
<Option value="BLOCKED">Заблокирован</Option>
|
||||
<Option value="DELETED">Удален</Option>
|
||||
<Select placeholder={t("statusMessage")}>
|
||||
<Option value="ACTIVE">{t("ACTIVE")}</Option>
|
||||
<Option value="DISABLED">{t("DISABLED")}</Option>
|
||||
<Option value="BLOCKED">{t("BLOCKED")}</Option>
|
||||
<Option value="DELETED">{t("DELETED")}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@ -213,14 +237,23 @@ export default function UserCreate() {
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
style={{ color: '#000' }}
|
||||
style={{ color: "#000" }}
|
||||
>
|
||||
<img
|
||||
src="/icons/drawer/reg.svg"
|
||||
alt="save"
|
||||
style={{ height: '18px', width: '18px' }}
|
||||
/>{' '}
|
||||
{t('addAccount')}
|
||||
{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>
|
||||
|
@ -1,47 +1,101 @@
|
||||
import { Button, Form, Input, Select } from 'antd';
|
||||
import { UserService } from '@/services/userService';
|
||||
import { useUserSelector } from '@/store/userStore';
|
||||
import { UserUpdate } from '@/types/user';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, message, Select, Spin } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function UserEdit() {
|
||||
interface UserEditProps {
|
||||
userId?: number;
|
||||
closeDrawer: () => void;
|
||||
}
|
||||
|
||||
export default function UserEdit({ userId, closeDrawer }: UserEditProps) {
|
||||
const currentUser = useUserSelector();
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useState<UserUpdate>({
|
||||
id: 0,
|
||||
name: '',
|
||||
login: '',
|
||||
email: '',
|
||||
password: '',
|
||||
bindTenantId: '',
|
||||
role: 'VIEWER',
|
||||
meta: {},
|
||||
createdAt: '',
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function getUser() {
|
||||
if (typeof userId === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const user = await UserService.getUserById(userId);
|
||||
setUser(user);
|
||||
form.setFieldsValue({ ...user });
|
||||
}
|
||||
|
||||
getUser();
|
||||
}, []);
|
||||
|
||||
const onFinish = async (values: UserUpdate) => {
|
||||
setLoading(true);
|
||||
let updatedUser: Partial<UserUpdate> = {};
|
||||
|
||||
(Object.keys(values) as Array<keyof UserUpdate>).forEach((key) => {
|
||||
if (values[key] !== user[key]) {
|
||||
updatedUser = { ...updatedUser, [key]: values[key] };
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(updatedUser).length > 0) {
|
||||
console.log('updateUser', userId, updatedUser);
|
||||
await UserService.updateUser(userId!, updatedUser);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
message.info(t('editAccountMessage'), 4);
|
||||
closeDrawer();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Form
|
||||
form={form}
|
||||
name="user-edit-form"
|
||||
layout="vertical"
|
||||
// onFinish={onFinish}
|
||||
initialValues={{
|
||||
name: 'Александр Александров',
|
||||
login: 'alexandralex@vorkout.ru',
|
||||
password: 'jKUUl776GHd',
|
||||
email: 'alexandralex@vorkout.ru',
|
||||
tenant: 'text',
|
||||
role: 'Директор магазина',
|
||||
status: 'Активен',
|
||||
}}
|
||||
onFinish={onFinish}
|
||||
initialValues={{ ...user }}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<Form.Item
|
||||
label={t('name')}
|
||||
name="name"
|
||||
rules={[{ required: true, message: t('nameMessage') }]}
|
||||
rules={[{ message: t('nameMessage') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('login')}
|
||||
name="login"
|
||||
rules={[{ required: true, message: t('loginMessage') }]}
|
||||
>
|
||||
<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={[{ required: true, message: t('passwordMessage') }]}
|
||||
rules={[{ message: t('passwordMessage') }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
@ -59,23 +113,27 @@ export default function UserEdit() {
|
||||
|
||||
<Form.Item
|
||||
label={t('tenant')}
|
||||
name="tenant"
|
||||
name="bindTenantId"
|
||||
rules={[{ required: true, message: t('tenantMessage') }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('role')}
|
||||
name="role"
|
||||
rules={[{ required: true, message: t('roleMessage') }]}
|
||||
>
|
||||
<Select placeholder={t('roleMessage')}>
|
||||
<Option value="Директор магазина">Директор магазина</Option>
|
||||
<Option value="Менеджер">Менеджер</Option>
|
||||
<Option value="Кассир">Кассир</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{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')}
|
||||
@ -83,10 +141,10 @@ export default function UserEdit() {
|
||||
rules={[{ required: true, message: t('statusMessage') }]}
|
||||
>
|
||||
<Select placeholder={t('statusMessage')}>
|
||||
<Option value="ACTIVE">Активен</Option>
|
||||
<Option value="DISABLED">Неактивен</Option>
|
||||
<Option value="BLOCKED">Заблокирован</Option>
|
||||
<Option value="DELETED">Удален</Option>
|
||||
<Option value="ACTIVE">{t('ACTIVE')}</Option>
|
||||
<Option value="DISABLED">{t('DISABLED')}</Option>
|
||||
<Option value="BLOCKED">{t('BLOCKED')}</Option>
|
||||
<Option value="DELETED">{t('DELETED')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
@ -99,12 +157,21 @@ export default function UserEdit() {
|
||||
block
|
||||
style={{ color: '#000' }}
|
||||
>
|
||||
<img
|
||||
src="/icons/drawer/save.svg"
|
||||
alt="save"
|
||||
style={{ height: '18px', width: '18px' }}
|
||||
/>{' '}
|
||||
{t('save')}
|
||||
{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>
|
||||
|
@ -37,6 +37,20 @@ i18n
|
||||
addAccount: 'Add account',
|
||||
save: 'Save changes',
|
||||
newAccount: 'New account',
|
||||
ACTIVE: 'Active',
|
||||
DISABLED: 'Disabled',
|
||||
BLOCKED: 'Blocked',
|
||||
DELETED: 'Deleted',
|
||||
OWNER: 'Owner',
|
||||
ADMIN: 'Admin',
|
||||
EDITOR: 'Editor',
|
||||
VIEWER: 'Viewer',
|
||||
nameLogin: 'Name, login',
|
||||
createdAt: 'Created',
|
||||
saving: 'Saving...',
|
||||
createdAccountMessage: 'User successfully created!',
|
||||
editAccountMessage: 'User successfully updated!',
|
||||
you: '(You)',
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
@ -66,6 +80,20 @@ i18n
|
||||
addAccount: 'Добавить аккаунт',
|
||||
save: 'Сохранить изменения',
|
||||
newAccount: 'Новая учетная запись',
|
||||
ACTIVE: 'Активен',
|
||||
DISABLED: 'Выключен',
|
||||
BLOCKED: 'Заблокирован',
|
||||
DELETED: 'Удален',
|
||||
OWNER: 'Владелец',
|
||||
ADMIN: 'Админ',
|
||||
EDITOR: 'Редактор',
|
||||
VIEWER: 'Наблюдатель',
|
||||
nameLogin: 'Имя, Логин',
|
||||
createdAt: 'Создано',
|
||||
saving: 'Сохранение...',
|
||||
createdAccountMessage: 'Пользователь успешно создан!',
|
||||
editAccountMessage: 'Пользователь успешно обновлен!',
|
||||
you: '(Вы)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1,39 +1,219 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User } from '@/types/user';
|
||||
import Header from '@/components/Header';
|
||||
import ContentDrawer from '@/components/ContentDrawer';
|
||||
import UserCreate from '@/components/UserCreate';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AccountStatus, AllUser, AllUserResponse } from "@/types/user";
|
||||
import Header from "@/components/Header";
|
||||
import ContentDrawer from "@/components/ContentDrawer";
|
||||
import UserCreate from "@/components/UserCreate";
|
||||
import { Avatar, Table } from "antd";
|
||||
import { TableProps } from "antd/lib";
|
||||
import { UserService } from "@/services/userService";
|
||||
import UserEdit from "@/components/UserEdit";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export default function AccountsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const showDrawer = () => setOpen(true);
|
||||
const closeDrawer = () => setOpen(false);
|
||||
const [activeAccount, setActiveAccount] = useState<
|
||||
{ login: string; id: number; name: string; email: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
const [accounts, setAccounts] = useState<User[]>([]);
|
||||
const showCreateDrawer = () => setOpenCreate(true);
|
||||
const closeCreateDrawer = () => {
|
||||
setActiveAccount(undefined);
|
||||
setOpenCreate(false);
|
||||
};
|
||||
const [openEdit, setOpenEdit] = useState(false);
|
||||
|
||||
const showEditDrawer = () => setOpenEdit(true);
|
||||
const closeEditDrawer = () => {
|
||||
setActiveAccount(undefined);
|
||||
setOpenEdit(false);
|
||||
};
|
||||
|
||||
const [accounts, setAccounts] = useState<AllUserResponse>({
|
||||
amountCount: 0,
|
||||
amountPages: 0,
|
||||
users: [],
|
||||
currentPage: 1,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
async function getUsers() {
|
||||
const page = Number(searchParams.get("page") || "1");
|
||||
const limit = Number(searchParams.get("limit") || "10");
|
||||
setSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
const data = await UserService.getUsers(page, limit);
|
||||
console.log("searchParams", searchParams);
|
||||
setAccounts(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, []);
|
||||
|
||||
const statusColor = {
|
||||
ACTIVE: "#27AE60",
|
||||
DISABLED: "#606060",
|
||||
BLOCKED: "#FF0000",
|
||||
DELETED: "#B30000",
|
||||
};
|
||||
|
||||
const columns: TableProps<AllUser>["columns"] = [
|
||||
{
|
||||
title: "#",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: t("nameLogin"),
|
||||
dataIndex: "nameLogin",
|
||||
key: "nameLogin",
|
||||
render: (text, record) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setActiveAccount({
|
||||
login: record.login,
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
email: record.email || "",
|
||||
});
|
||||
showEditDrawer();
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "32px",
|
||||
width: "32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
src={`https://gamma.heado.ru/go/ava?name=${record.login}`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div>{record.name}</div>
|
||||
<div style={{ color: "#606060" }}>{record.login}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "E-mail",
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: t("tenant"),
|
||||
dataIndex: "bindTenantId",
|
||||
key: "tenant",
|
||||
},
|
||||
{
|
||||
title: t("role"),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
render: (text) => <div>{t(text)}</div>,
|
||||
},
|
||||
{
|
||||
title: t("createdAt"),
|
||||
dataIndex: "createdAt",
|
||||
key: "createdAt",
|
||||
render: (text) => (
|
||||
<div>
|
||||
{new Date(text).toLocaleString("ru", {
|
||||
year: "2-digit",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
render: (text) => (
|
||||
<div style={{ color: statusColor[text as AccountStatus] }}>
|
||||
{t(text)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const onTableChange: TableProps<AllUser>["onChange"] = (pagination) => {
|
||||
console.log(pagination);
|
||||
UserService.getUsers(
|
||||
pagination.current as number,
|
||||
pagination.pageSize
|
||||
).then((data) => {
|
||||
setAccounts(data);
|
||||
setSearchParams({
|
||||
page: data.currentPage.toString(),
|
||||
limit: data.limit.toString(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title={t('accounts')}
|
||||
title={t("accounts")}
|
||||
additionalContent={
|
||||
<img
|
||||
src="./icons/header/add_2.svg"
|
||||
alt="add"
|
||||
style={{
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
cursor: 'pointer',
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={showDrawer}
|
||||
onClick={showCreateDrawer}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Table
|
||||
size="small"
|
||||
onChange={onTableChange}
|
||||
columns={columns}
|
||||
dataSource={accounts.users}
|
||||
pagination={{
|
||||
pageSize: accounts.limit,
|
||||
current: accounts.currentPage,
|
||||
total: accounts.amountCount,
|
||||
}}
|
||||
rowKey={"id"}
|
||||
/>
|
||||
|
||||
<ContentDrawer open={open} closeDrawer={closeDrawer} type="create">
|
||||
<UserCreate />
|
||||
<ContentDrawer
|
||||
open={openCreate}
|
||||
closeDrawer={closeCreateDrawer}
|
||||
type="create"
|
||||
>
|
||||
<UserCreate getUsers={getUsers} closeDrawer={closeCreateDrawer} />
|
||||
</ContentDrawer>
|
||||
<ContentDrawer
|
||||
login={activeAccount?.login}
|
||||
name={activeAccount?.name}
|
||||
email={activeAccount?.email}
|
||||
open={openEdit}
|
||||
closeDrawer={closeEditDrawer}
|
||||
type="edit"
|
||||
>
|
||||
<UserEdit userId={activeAccount?.id} closeDrawer={closeEditDrawer} />
|
||||
</ContentDrawer>
|
||||
</>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Typography } from 'antd';
|
||||
import { Form, Input, Button, Typography, message } from 'antd';
|
||||
import {
|
||||
EyeInvisibleOutlined,
|
||||
EyeTwoTone,
|
||||
@ -45,7 +45,11 @@ export default function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form name="login" onFinish={onFinish} layout="vertical">
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="login"
|
||||
rules={[{ required: true, message: 'Введите login' }]}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import api from '@/api/api';
|
||||
import { User } from '@/types/user';
|
||||
import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user';
|
||||
|
||||
export class UserService {
|
||||
static async getProfile(): Promise<User> {
|
||||
@ -9,8 +9,30 @@ export class UserService {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async getUsers(page: number = 1, limit: number = 10): Promise<any> {
|
||||
const users = api.getUsers(page, limit);
|
||||
return users;
|
||||
static async getUsers(
|
||||
page: number = 1,
|
||||
limit: number = 10
|
||||
): Promise<AllUserResponse> {
|
||||
console.log('getUsers');
|
||||
const allUsers = api.getUsers(page, limit);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
static async getUserById(userId: number): Promise<User> {
|
||||
console.log('getUserById');
|
||||
const user = api.getUserById(userId);
|
||||
return user;
|
||||
}
|
||||
|
||||
static async createUser(user: UserCreate): Promise<User> {
|
||||
console.log('createUser');
|
||||
const createdUser = api.createUser(user);
|
||||
return createdUser;
|
||||
}
|
||||
|
||||
static async updateUser(userId: number, user: UserUpdate): Promise<User> {
|
||||
console.log('updateUser');
|
||||
const updatedUser = api.updateUser(userId, user);
|
||||
return updatedUser;
|
||||
}
|
||||
}
|
||||
|
@ -191,6 +191,10 @@ export interface components {
|
||||
amountCount: number;
|
||||
/** Amountpages */
|
||||
amountPages: number;
|
||||
/** Currentpage */
|
||||
currentPage: number;
|
||||
/** Limit */
|
||||
limit: number;
|
||||
};
|
||||
/** Auth */
|
||||
Auth: {
|
||||
@ -247,6 +251,25 @@ export interface components {
|
||||
createdAt: string;
|
||||
status: components["schemas"]["AccountStatus"];
|
||||
};
|
||||
/** UserCreate */
|
||||
UserCreate: {
|
||||
/** Name */
|
||||
name?: string | null;
|
||||
/** Login */
|
||||
login?: string | null;
|
||||
/** Email */
|
||||
email?: string | null;
|
||||
/** Password */
|
||||
password?: string | null;
|
||||
/** Bindtenantid */
|
||||
bindTenantId?: string | null;
|
||||
role?: components["schemas"]["AccountRole"] | null;
|
||||
/** Meta */
|
||||
meta?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
status?: components["schemas"]["AccountStatus"] | null;
|
||||
};
|
||||
/** UserUpdate */
|
||||
UserUpdate: {
|
||||
/** Id */
|
||||
@ -257,6 +280,8 @@ export interface components {
|
||||
login?: string | null;
|
||||
/** Email */
|
||||
email?: string | null;
|
||||
/** Password */
|
||||
password?: string | null;
|
||||
/** Bindtenantid */
|
||||
bindTenantId?: string | null;
|
||||
role?: components["schemas"]["AccountRole"] | null;
|
||||
@ -435,7 +460,7 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
"application/json": components["schemas"]["UserCreate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
@ -445,7 +470,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["AllUser"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -476,7 +501,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
@ -511,7 +536,7 @@ export interface operations {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["User"];
|
||||
"application/json": components["schemas"]["UserUpdate"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
@ -1,3 +1,9 @@
|
||||
import { components } from './openapi-types';
|
||||
|
||||
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'];
|
||||
|
Loading…
Reference in New Issue
Block a user
Может тут два пароля с разными id создаться, получается?
Нет, не должны
Есть какие-то предпосылки? Я просто не вижу такой возможности
Ну, просто в моменте у нас может быть несколько ключей одного типа (API KEY как минимум), но с разными key_id, и по идее уникальность тут обеспечивается в том числе key_id, т.е. если key_type:PASSWORD присваивать разные key_id, то в случае двойной сработки create_password_key (я не увидел блокировки на двойной вызов этого метода чисто из-за сбоя сетевого стека, к примеру), то мы получим два ключа key_type:PASSWORD ?
решается тем, что паролю надо key_id прописывать статичный, кмк.
Тогда на основе чего его генерить?
У нас там сейчас
f"{datetime.now().strftime('%Y-%m-%d')}-{random_number}"
, гдеrandom_number
четырехзначное число, брать id пользователя?А я не вижу необходимости генерить его конкретно для PASSWORD, просто прописать статичный литерал 'password' к примеру и сделать фоллбэк в случае, если при создании key_type: PASSWORD на конкретном пользователе уже существует такой ключ, я бы сделал его либо на обновление, либо просто пропуск.