From a3ee18f6fdb4bedeff8938e320cae314c8d91a5a Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 13:00:40 +0500 Subject: [PATCH 01/31] feat: add accounts table --- client/src/components/Header.tsx | 4 +- client/src/config/i18n.ts | 20 ++++++ client/src/pages/AccountsPage.tsx | 101 ++++++++++++++++++++++++++++- client/src/services/userService.ts | 8 +-- client/src/types/user.ts | 5 ++ 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 2ff9076..f640cb1 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,3 +1,4 @@ +import { useUserSelector } from '@/store/userStore'; import { Avatar } from 'antd'; import Title from 'antd/es/typography/Title'; @@ -7,6 +8,7 @@ interface HeaderProps { } export default function Header({ title, additionalContent }: HeaderProps) { + const user = useUserSelector(); return (
diff --git a/client/src/config/i18n.ts b/client/src/config/i18n.ts index 09bd8b8..db55a45 100644 --- a/client/src/config/i18n.ts +++ b/client/src/config/i18n.ts @@ -37,6 +37,16 @@ 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', }, }, ru: { @@ -66,6 +76,16 @@ i18n addAccount: 'Добавить аккаунт', save: 'Сохранить изменения', newAccount: 'Новая учетная запись', + ACTIVE: 'Активен', + DISABLED: 'Выключен', + BLOCKED: 'Заблокирован', + DELETED: 'Удален', + OWNER: 'Владелец', + ADMIN: 'Админ', + EDITOR: 'Редактор', + VIEWER: 'Наблюдатель', + nameLogin: 'Имя, Логин', + createdAt: 'Создано', }, }, }, diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index 02d85fd..aa67ac7 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -1,9 +1,12 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { User } from '@/types/user'; +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'; export default function AccountsPage() { const { t } = useTranslation(); @@ -12,7 +15,93 @@ export default function AccountsPage() { const showDrawer = () => setOpen(true); const closeDrawer = () => setOpen(false); - const [accounts, setAccounts] = useState([]); + const [accounts, setAccounts] = useState({ + amountCount: 0, + amountPages: 0, + users: [], + }); + + useEffect(() => { + async function getUsers() { + const data = await UserService.getUsers(); + setAccounts(data); + } + + getUsers(); + }, []); + + const statusColor = { + ACTIVE: '#27AE60', + DISABLED: '#606060', + BLOCKED: '#FF0000', + DELETED: '#B30000', + }; + + const columns: TableProps['columns'] = [ + { + title: '#', + dataIndex: 'id', + key: 'id', + }, + { + title: t('nameLogin'), + dataIndex: 'nameLogin', + key: 'nameLogin', + render: (text, record) => ( +
+
+ +
+
+
{record.name}
+
{record.email}
+
+
+ ), + }, + { + title: 'E-mail', + dataIndex: 'email', + key: 'email', + }, + { + title: t('tenant'), + dataIndex: 'bindTenantId', + key: 'tenant', + }, + { + title: t('role'), + dataIndex: 'role', + key: 'role', + render: (text) =>
{t(text)}
, + }, + { + title: t('createdAt'), + dataIndex: 'createdAt', + key: 'createdAt', + }, + { + title: t('status'), + dataIndex: 'status', + key: 'status', + render: (text) => ( +
+ {t(text)} +
+ ), + }, + ]; return ( <> @@ -31,6 +120,12 @@ export default function AccountsPage() { /> } /> + diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts index 3063b47..fbfbb29 100644 --- a/client/src/services/userService.ts +++ b/client/src/services/userService.ts @@ -1,5 +1,5 @@ import api from '@/api/api'; -import { User } from '@/types/user'; +import { AllUserResponse, User } from '@/types/user'; export class UserService { static async getProfile(): Promise { @@ -9,8 +9,8 @@ export class UserService { return user; } - static async getUsers(page: number = 1, limit: number = 10): Promise { - const users = api.getUsers(page, limit); - return users; + static async getUsers(page: number = 1, limit: number = 10): Promise { + const allUsers = api.getUsers(page, limit); + return allUsers; } } diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 10b18d4..0c20246 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -1,3 +1,8 @@ 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']; + From 18bb79262cb1f836ca69fdced2bee59cdfc55bc1 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 13:12:31 +0500 Subject: [PATCH 02/31] feat(api): add current page to AllUserResponse and fix returning type --- api/api/db/logic/account.py | 4 ++-- api/api/schemas/endpoints/account.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/api/db/logic/account.py b/api/api/db/logic/account.py index 5f16ac4..06d42a6 100644 --- a/api/api/db/logic/account.py +++ b/api/api/db/logic/account.py @@ -13,7 +13,7 @@ from api.schemas.account.account import User from api.schemas.endpoints.account import AllUserResponse, all_user_adapter -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,7 +47,7 @@ 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) async def get_user_by_id(connection: AsyncConnection, id: int) -> Optional[User]: diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 37ceeea..6366ce7 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -35,6 +35,7 @@ class AllUserResponse(Base): users: List[AllUser] amount_count: int amount_pages: int + current_page: int all_user_adapter = TypeAdapter(List[AllUser]) From 7c2c4071cc844ba39998f922f8b1546a33a2cf57 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 13:16:31 +0500 Subject: [PATCH 03/31] feat(Makefile): add regenerate-openapi-local command --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index c0e1a7f..3f4dab6 100644 --- a/Makefile +++ b/Makefile @@ -62,3 +62,7 @@ format-api: check-api: cd api && \ poetry run ruff format . --check + +regenerate-openapi-local: + rm client/src/types/openapi-types.types \ + npx openapi-typescript http://localhost:8000/openapi -o client/src/types/openapi-types.ts From aae56a8c736e365fd58968525e1d42ea66833768 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 13:18:45 +0500 Subject: [PATCH 04/31] feat(AccountsPage): add on table change --- Makefile | 2 +- client/src/pages/AccountsPage.tsx | 15 ++++++++++++++- client/src/types/openapi-types.ts | 2 ++ client/src/types/user.ts | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3f4dab6..241334c 100644 --- a/Makefile +++ b/Makefile @@ -64,5 +64,5 @@ check-api: poetry run ruff format . --check regenerate-openapi-local: - rm client/src/types/openapi-types.types \ + rm client/src/types/openapi-types.ts \ npx openapi-typescript http://localhost:8000/openapi -o client/src/types/openapi-types.ts diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index aa67ac7..f94ad6c 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -19,6 +19,7 @@ export default function AccountsPage() { amountCount: 0, amountPages: 0, users: [], + currentPage: 1, }); useEffect(() => { @@ -103,6 +104,13 @@ export default function AccountsPage() { }, ]; + const onTableChange: TableProps['onChange'] = (pagination) => { + console.log(pagination); + UserService.getUsers(pagination.current as number, 10).then((data) => { + setAccounts(data); + }); + }; + return ( <>
diff --git a/client/src/types/openapi-types.ts b/client/src/types/openapi-types.ts index b23e43f..1e14c6c 100644 --- a/client/src/types/openapi-types.ts +++ b/client/src/types/openapi-types.ts @@ -191,6 +191,8 @@ export interface components { amountCount: number; /** Amountpages */ amountPages: number; + /** Currentpage */ + currentPage: number; }; /** Auth */ Auth: { diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 0c20246..ef49e4a 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -1,4 +1,4 @@ -import { components } from './openapi-types'; +import { components } from "./openapi-types"; export type User = components['schemas']['User']; export type AllUserResponse = components['schemas']['AllUserResponse']; From e5dfdc346467327faad7cb09106a507ed1b1992f Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 16:23:43 +0500 Subject: [PATCH 05/31] feat(AccountsPage): add userEdit --- client/src/api/api.ts | 5 +++ client/src/components/ContentDrawer.tsx | 15 +++++-- client/src/components/SiderMenu.tsx | 22 ++++++---- client/src/components/UserCreate.tsx | 18 +++++--- client/src/components/UserEdit.tsx | 54 +++++++++++++++-------- client/src/pages/AccountsPage.tsx | 57 ++++++++++++++++++++++--- client/src/services/userService.ts | 12 +++++- 7 files changed, 139 insertions(+), 44 deletions(-) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 76641c0..37824a7 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -109,6 +109,11 @@ const api = { ); return response.data; }, + + async getUserById(userId: number): Promise { + const response = await base.get(`/account/${userId}`); + return response.data; + }, }; export default api; diff --git a/client/src/components/ContentDrawer.tsx b/client/src/components/ContentDrawer.tsx index f02b6f4..5bd542c 100644 --- a/client/src/components/ContentDrawer.tsx +++ b/client/src/components/ContentDrawer.tsx @@ -8,6 +8,9 @@ interface ContentDrawerProps { closeDrawer: () => void; children: React.ReactNode; type: 'create' | 'edit'; + login?: string; + name?: string; + email?: string; } export default function ContentDrawer({ @@ -15,6 +18,9 @@ export default function ContentDrawer({ closeDrawer, children, type, + login, + name, + email, }: ContentDrawerProps) { const { t } = useTranslation(); const [width, setWidth] = useState('30%'); @@ -59,16 +65,18 @@ export default function ContentDrawer({
- Александр Александров + {name} - alexandralex@vorkout.ru + {email}
@@ -152,7 +160,6 @@ export default function ContentDrawer({ placement="right" open={open} width={width} - destroyOnClose={true} closable={false} > {children} diff --git a/client/src/components/SiderMenu.tsx b/client/src/components/SiderMenu.tsx index 8ce7ab4..af8ca38 100644 --- a/client/src/components/SiderMenu.tsx +++ b/client/src/components/SiderMenu.tsx @@ -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 ? ( - {t('accounts')} - ) : ( - t('accounts') - ), - style: collapseStyle, - }, + user && (user.role === 'OWNER' || user.role === 'ADMIN') + ? { + key: '/accounts', + label: !collapsed ? ( + {t('accounts')} + ) : ( + t('accounts') + ), + style: collapseStyle, + } + : undefined, { key: '/events-list', label: !collapsed ? ( diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index edf09ab..a2db419 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -11,6 +11,7 @@ import { } from 'antd'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useUserSelector } from '@/store/userStore'; const { Option } = Select; @@ -25,6 +26,7 @@ const getBase64 = (file: FileType): Promise => }); export default function UserCreate() { + const user = useUserSelector(); const { t } = useTranslation(); const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(''); @@ -187,9 +189,11 @@ export default function UserCreate() { rules={[{ required: true, message: t('roleMessage') }]} > @@ -199,10 +203,10 @@ export default function UserCreate() { rules={[{ required: true, message: t('statusMessage') }]} > diff --git a/client/src/components/UserEdit.tsx b/client/src/components/UserEdit.tsx index bd5e8f0..47f11ef 100644 --- a/client/src/components/UserEdit.tsx +++ b/client/src/components/UserEdit.tsx @@ -1,25 +1,43 @@ +import { UserService } from '@/services/userService'; +import { useUserSelector } from '@/store/userStore'; +import { User } from '@/types/user'; import { Button, Form, Input, Select } from 'antd'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; const { Option } = Select; -export default function UserEdit() { +interface UserEditProps { + userId?: number; +} + +export default function UserEdit({ userId }: UserEditProps) { + const currentUser = useUserSelector(); + const [form] = Form.useForm(); const { t } = useTranslation(); + const [user, setUser] = useState(null); + + useEffect(() => { + async function getUser() { + if (typeof userId === 'undefined') { + return; + } + const user = await UserService.getUserById(userId); + setUser(user); + form.setFieldsValue({ ...user }); + } + + getUser(); + }, []); + return (
@@ -83,10 +103,10 @@ export default function UserEdit() { rules={[{ required: true, message: t('statusMessage') }]} > diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index f94ad6c..b21c91d 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -7,13 +7,28 @@ 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'; export default function AccountsPage() { const { t } = useTranslation(); - const [open, setOpen] = useState(false); + const [openCreate, setOpenCreate] = useState(false); - const showDrawer = () => setOpen(true); - const closeDrawer = () => setOpen(false); + 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({ amountCount: 0, @@ -49,7 +64,23 @@ export default function AccountsPage() { dataIndex: 'nameLogin', key: 'nameLogin', render: (text, record) => ( -
+
{ + setActiveAccount({ + login: record.login, + id: record.id, + name: record.name, + email: record.email || '', + }); + showEditDrawer(); + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: '16px', + cursor: 'pointer', + }} + >
} /> @@ -140,9 +171,23 @@ export default function AccountsPage() { rowKey={'id'} /> - + + + + ); } diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts index fbfbb29..4e11c19 100644 --- a/client/src/services/userService.ts +++ b/client/src/services/userService.ts @@ -9,8 +9,18 @@ export class UserService { return user; } - static async getUsers(page: number = 1, limit: number = 10): Promise { + static async getUsers( + page: number = 1, + limit: number = 10 + ): Promise { + console.log('getUsers'); const allUsers = api.getUsers(page, limit); return allUsers; } + + static async getUserById(userId: number): Promise { + console.log('getUserById'); + const user = api.getUserById(userId); + return user; + } } From 448e4264a5e054fbeb16df687344dea20c3b3642 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 24 Jun 2025 16:29:19 +0500 Subject: [PATCH 06/31] feat(AccountPage): add destroyOnHidden to ContentDrawer and fix tenant and login --- client/src/components/ContentDrawer.tsx | 1 + client/src/components/UserEdit.tsx | 2 +- client/src/pages/AccountsPage.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/ContentDrawer.tsx b/client/src/components/ContentDrawer.tsx index 5bd542c..79b9cf5 100644 --- a/client/src/components/ContentDrawer.tsx +++ b/client/src/components/ContentDrawer.tsx @@ -160,6 +160,7 @@ export default function ContentDrawer({ placement="right" open={open} width={width} + destroyOnHidden={true} closable={false} > {children} diff --git a/client/src/components/UserEdit.tsx b/client/src/components/UserEdit.tsx index 47f11ef..8bcef4e 100644 --- a/client/src/components/UserEdit.tsx +++ b/client/src/components/UserEdit.tsx @@ -77,7 +77,7 @@ export default function UserEdit({ userId }: UserEditProps) { diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index b21c91d..e0f6eef 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -97,7 +97,7 @@ export default function AccountsPage() {
{record.name}
-
{record.email}
+
{record.login}
), From 8f5dd07bf5bd76dbe2fd99914cfadeb96b93383f Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 25 Jun 2025 13:38:43 +0500 Subject: [PATCH 07/31] feat(client): add create user --- Makefile | 5 +++-- client/src/api/api.ts | 7 ++++++- client/src/components/UserCreate.tsx | 16 ++++++++++++++-- client/src/pages/AccountsPage.tsx | 6 ++++-- client/src/services/userService.ts | 8 +++++++- client/src/types/openapi-types.ts | 2 ++ client/src/types/user.ts | 4 ++-- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 241334c..864d9e9 100644 --- a/Makefile +++ b/Makefile @@ -64,5 +64,6 @@ check-api: poetry run ruff format . --check regenerate-openapi-local: - rm client/src/types/openapi-types.ts \ - npx openapi-typescript http://localhost:8000/openapi -o client/src/types/openapi-types.ts + cd client \ + rm src/types/openapi-types.ts \ + npx openapi-typescript http://localhost:8000/openapi -o src/types/openapi-types.ts diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 37824a7..1fef22c 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -4,7 +4,7 @@ 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, UserUpdate } from '@/types/user'; const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${ import.meta.env.VITE_APP_API_URL @@ -114,6 +114,11 @@ const api = { const response = await base.get(`/account/${userId}`); return response.data; }, + + async createUser(user: UserUpdate): Promise { + const response = await base.post('/account', user); + return response.data; + }, }; export default api; diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index a2db419..9ddc6a6 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -12,6 +12,8 @@ import { import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useUserSelector } from '@/store/userStore'; +import { UserUpdate } from '@/types/user'; +import { UserService } from '@/services/userService'; const { Option } = Select; @@ -25,7 +27,11 @@ const getBase64 = (file: FileType): Promise => reader.onerror = (error) => reject(error); }); -export default function UserCreate() { +interface UserCreateProps { + closeDrawer: () => void; +} + +export default function UserCreate({ closeDrawer }: UserCreateProps) { const user = useUserSelector(); const { t } = useTranslation(); const [previewOpen, setPreviewOpen] = useState(false); @@ -45,6 +51,12 @@ export default function UserCreate() { const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => setFileList(newFileList); + const onFinish = async (values: UserUpdate) => { + console.log(values); + await UserService.createUser(values); + closeDrawer(); + }; + const customUploadButton = (
{ @@ -160,11 +161,12 @@ export default function AccountsPage() { } />
- + { @@ -23,4 +23,10 @@ export class UserService { const user = api.getUserById(userId); return user; } + + static async createUser(user: UserUpdate): Promise { + console.log('createUser'); + const createdUser = api.createUser(user); + return createdUser; + } } diff --git a/client/src/types/openapi-types.ts b/client/src/types/openapi-types.ts index 1e14c6c..79eb692 100644 --- a/client/src/types/openapi-types.ts +++ b/client/src/types/openapi-types.ts @@ -193,6 +193,8 @@ export interface components { amountPages: number; /** Currentpage */ currentPage: number; + /** Limit */ + limit: number; }; /** Auth */ Auth: { diff --git a/client/src/types/user.ts b/client/src/types/user.ts index ef49e4a..1251ddf 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -1,8 +1,8 @@ -import { components } from "./openapi-types"; +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']; From 53bf1733731a030423ffdfe083c41042862a773b Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 25 Jun 2025 13:39:16 +0500 Subject: [PATCH 08/31] fix(api): fix get_user_by_id method --- api/api/db/logic/account.py | 44 +++++++++++++--------------- api/api/endpoints/account.py | 38 ++++++++++++------------ api/api/schemas/endpoints/account.py | 1 + 3 files changed, 39 insertions(+), 44 deletions(-) diff --git a/api/api/db/logic/account.py b/api/api/db/logic/account.py index 06d42a6..e6fe163 100644 --- a/api/api/db/logic/account.py +++ b/api/api/db/logic/account.py @@ -1,16 +1,14 @@ -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, AllUserResponse, UserUpdate async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]: @@ -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, current_page=page) + 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[UserUpdate]: """ Получает юзера по 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 UserUpdate.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: UserUpdate, creator_id: int) -> Optional[UserUpdate]: """ Создает нове поле в таблице account_table. """ @@ -123,8 +118,9 @@ async def create_user(connection: AsyncConnection, user: User, creator_id: int) status=user.status.value, ) - await connection.execute(query) + res = await connection.execute(query) await connection.commit() + user = await get_user_by_id(connection, res.lastrowid) return user diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index 99ecf83..77df6dd 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -4,29 +4,23 @@ 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.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 AllUserResponse, 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", @@ -53,7 +47,9 @@ async def get_all_account( @api_router.get("/{user_id}", dependencies=[Depends(bearer_schema)], response_model=User) async def get_account( - user_id: int, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user) + user_id: int, + connection: AsyncConnection = Depends(get_connection_dep), + current_user=Depends(get_current_user), ): authorize_user = await db_user_role_validation(connection, current_user) @@ -65,19 +61,19 @@ 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=UserUpdate) async def create_account( - user: UserUpdate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user) + user: UserUpdate, + 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) + return new_user else: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="An account with this information already exists." @@ -113,7 +109,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) diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 6366ce7..4817122 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -36,6 +36,7 @@ class AllUserResponse(Base): amount_count: int amount_pages: int current_page: int + limit: int all_user_adapter = TypeAdapter(List[AllUser]) From 919758ab691fec22e242778e9523bb0c9c74bb32 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 25 Jun 2025 13:54:48 +0500 Subject: [PATCH 09/31] fix: bind tenant id --- api/api/endpoints/account.py | 2 +- client/src/components/UserCreate.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index 77df6dd..6bc6dd4 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -45,7 +45,7 @@ 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), diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index 9ddc6a6..996b9df 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -146,7 +146,7 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { login: '', password: '', email: '', - tenant: '', + bindTenantId: '', role: '', status: '', }} @@ -189,7 +189,7 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { From a7e813b447d7898263bf53338d91d1b78b8b448d Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 25 Jun 2025 14:09:51 +0500 Subject: [PATCH 10/31] feat(UserCreate): add loading animation --- client/src/components/UserCreate.tsx | 29 +++++++++++++++++++++------- client/src/config/i18n.ts | 4 ++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index 996b9df..a1cd8f8 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -8,12 +8,15 @@ import { UploadFile, GetProp, UploadProps, + message, + Spin, } from 'antd'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useUserSelector } from '@/store/userStore'; import { UserUpdate } from '@/types/user'; import { UserService } from '@/services/userService'; +import { LoadingOutlined } from '@ant-design/icons'; const { Option } = Select; @@ -36,6 +39,7 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { const { t } = useTranslation(); const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(''); + const [loading, setLoading] = useState(false); const [fileList, setFileList] = useState([]); @@ -52,9 +56,11 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { setFileList(newFileList); const onFinish = async (values: UserUpdate) => { - console.log(values); + setLoading(true); await UserService.createUser(values); closeDrawer(); + setLoading(false); + message.info(t('createdAccountMessage'), 4); }; const customUploadButton = ( @@ -231,12 +237,21 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { block style={{ color: '#000' }} > - save{' '} - {t('addAccount')} + {loading ? ( + <> + } size="small">{' '} + {t('saving')} + + ) : ( + <> + save{' '} + {t('addAccount')} + + )} diff --git a/client/src/config/i18n.ts b/client/src/config/i18n.ts index db55a45..bc44352 100644 --- a/client/src/config/i18n.ts +++ b/client/src/config/i18n.ts @@ -47,6 +47,8 @@ i18n VIEWER: 'Viewer', nameLogin: 'Name, login', createdAt: 'Created', + saving: 'Saving...', + createdAccountMessage: 'User successfully created!', }, }, ru: { @@ -86,6 +88,8 @@ i18n VIEWER: 'Наблюдатель', nameLogin: 'Имя, Логин', createdAt: 'Создано', + saving: 'Сохранение...', + createdAccountMessage: 'Пользователь успешно создан!' }, }, }, From 6c0a6ac1d4c261325fba48cfbdcee8ec6b4d91ae Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 25 Jun 2025 21:00:51 +0500 Subject: [PATCH 11/31] feat(AccountsPage): add change page size --- client/src/components/ContentDrawer.tsx | 10 ++++++++-- client/src/config/i18n.ts | 4 +++- client/src/pages/AccountsPage.tsx | 5 ++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/src/components/ContentDrawer.tsx b/client/src/components/ContentDrawer.tsx index 79b9cf5..678295d 100644 --- a/client/src/components/ContentDrawer.tsx +++ b/client/src/components/ContentDrawer.tsx @@ -2,6 +2,7 @@ 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; @@ -22,6 +23,7 @@ export default function ContentDrawer({ name, email, }: ContentDrawerProps) { + const user = useUserSelector(); const { t } = useTranslation(); const [width, setWidth] = useState('30%'); @@ -36,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 = (
- - {name} + + {name} {login === user?.login ? t('you') : ''} {email} diff --git a/client/src/config/i18n.ts b/client/src/config/i18n.ts index bc44352..360cde1 100644 --- a/client/src/config/i18n.ts +++ b/client/src/config/i18n.ts @@ -49,6 +49,7 @@ i18n createdAt: 'Created', saving: 'Saving...', createdAccountMessage: 'User successfully created!', + you: '(You)', }, }, ru: { @@ -89,7 +90,8 @@ i18n nameLogin: 'Имя, Логин', createdAt: 'Создано', saving: 'Сохранение...', - createdAccountMessage: 'Пользователь успешно создан!' + createdAccountMessage: 'Пользователь успешно создан!', + you: '(Вы)', }, }, }, diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index c226b64..317cfee 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -138,7 +138,10 @@ export default function AccountsPage() { const onTableChange: TableProps['onChange'] = (pagination) => { console.log(pagination); - UserService.getUsers(pagination.current as number, 10).then((data) => { + UserService.getUsers( + pagination.current as number, + pagination.pageSize + ).then((data) => { setAccounts(data); }); }; From febac9659f2071ba88a15a84dd006a537d8ab99e Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 12:27:14 +0500 Subject: [PATCH 12/31] refactor(Makefile): change source with poetry --- Makefile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 864d9e9..f5ac720 100644 --- a/Makefile +++ b/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,24 @@ start-client: migrate: cd api && \ - source .venv/bin/activate && \ + poetry env activate && \ cd $(API_APPLICATION_NAME)/db && \ PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic upgrade $(args) downgrade: cd api && \ - source .venv/bin/activate && \ + poetry env activate && \ cd $(API_APPLICATION_NAME)/db && \ PYTHONPATH='../..' alembic downgrade -1 revision: cd api && \ - source .venv/bin/activate && \ + poetry env activate && \ cd $(API_APPLICATION_NAME)/db && \ PYTHONPATH='../..' ALEMBIC_MIGRATIONS=True alembic revision --autogenerate venv-api: cd api && \ - poetry env activate \ poetry install venv-client: From 22064d2b52c874fe50b2960fa2dd7e6d096ccc82 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 15:04:43 +0500 Subject: [PATCH 13/31] feat(types): generate new types --- client/src/types/openapi-types.ts | 25 ++++++++++++++++++++++--- client/src/types/user.ts | 1 + 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/client/src/types/openapi-types.ts b/client/src/types/openapi-types.ts index 79eb692..c203144 100644 --- a/client/src/types/openapi-types.ts +++ b/client/src/types/openapi-types.ts @@ -251,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 */ @@ -439,7 +458,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["UserUpdate"]; + "application/json": components["schemas"]["UserCreate"]; }; }; responses: { @@ -449,7 +468,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["User"]; + "application/json": components["schemas"]["AllUser"]; }; }; /** @description Validation Error */ @@ -480,7 +499,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["User"]; + "application/json": components["schemas"]["UserUpdate"]; }; }; /** @description Validation Error */ diff --git a/client/src/types/user.ts b/client/src/types/user.ts index 1251ddf..6a3f506 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -6,3 +6,4 @@ 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']; From 692461e266881e0700567ea0b59f79f021b98957 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 15:05:28 +0500 Subject: [PATCH 14/31] feat: create new user with password --- api/api/db/logic/account.py | 16 +++++++++------- api/api/db/logic/auth.py | 5 +++-- api/api/db/logic/keyring.py | 25 +++++++++++++++++++++---- api/api/endpoints/account.py | 6 +++--- api/api/schemas/endpoints/account.py | 15 +++++++++++++-- api/api/services/auth.py | 16 +++++++--------- api/api/utils/hasher.py | 9 +++++++++ api/api/utils/init.py | 19 +++++-------------- client/src/api/api.ts | 8 +++++--- client/src/components/UserCreate.tsx | 5 +++-- client/src/services/userService.ts | 4 ++-- 11 files changed, 80 insertions(+), 48 deletions(-) diff --git a/api/api/db/logic/account.py b/api/api/db/logic/account.py index e6fe163..78be803 100644 --- a/api/api/db/logic/account.py +++ b/api/api/db/logic/account.py @@ -6,9 +6,10 @@ from typing import Optional from sqlalchemy import func, insert, select from sqlalchemy.ext.asyncio import AsyncConnection +from api.db.logic.keyring import create_password_key from api.db.tables.account import account_table from api.schemas.account.account import User -from api.schemas.endpoints.account import all_user_adapter, AllUserResponse, UserUpdate +from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate, UserUpdate async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]: @@ -54,7 +55,7 @@ async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Opt ) -async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[UserUpdate]: +async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[AllUser]: """ Получает юзера по id. """ @@ -66,7 +67,7 @@ async def get_user_by_id(connection: AsyncConnection, user_id: int) -> Optional[ if not user: return None - return UserUpdate.model_validate(user) + return AllUser.model_validate(user) async def get_user_by_login(connection: AsyncConnection, login: str) -> Optional[User]: @@ -102,7 +103,7 @@ async def update_user_by_id(connection: AsyncConnection, update_values, user) -> await connection.commit() -async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id: int) -> Optional[UserUpdate]: +async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: int) -> Optional[AllUser]: """ Создает нове поле в таблице account_table. """ @@ -112,7 +113,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id: 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, @@ -121,6 +122,7 @@ async def create_user(connection: AsyncConnection, user: UserUpdate, creator_id: res = await connection.execute(query) await connection.commit() - user = await get_user_by_id(connection, res.lastrowid) + new_user = await get_user_by_id(connection, res.lastrowid) + await create_password_key(connection, user.password, new_user.id) - return user + return new_user diff --git a/api/api/db/logic/auth.py b/api/api/db/logic/auth.py index 20e9573..bb02986 100644 --- a/api/api/db/logic/auth.py +++ b/api/api/db/logic/auth.py @@ -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 diff --git a/api/api/db/logic/keyring.py b/api/api/db/logic/keyring.py index 91469a6..4856d0a 100644 --- a/api/api/db/logic/keyring.py +++ b/api/api/db/logic/keyring.py @@ -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.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 +from api.utils.key_id_gen import KeyIdGenerator async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]: @@ -67,3 +68,19 @@ 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() + stmt = insert(account_keyring_table).values( + owner_id=owner_id, + key_type=KeyType.PASSWORD.value, + key_id=KeyIdGenerator(), + key_value=hasher.hash_data(password), + created_at=datetime.now(timezone.utc), + expiry=datetime.now(timezone.utc) + timedelta(days=365), + status=KeyStatus.ACTIVE.value, + ) + await connection.execute(stmt) + await connection.commit() diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index 6bc6dd4..f16d750 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -17,7 +17,7 @@ from api.db.logic.account import ( 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 AllUserResponse, UserUpdate +from api.schemas.endpoints.account import AllUser, AllUserResponse, UserCreate, UserUpdate from api.services.auth import get_current_user from api.services.update_data_validation import update_user_data_changes from api.services.user_role_validation import db_user_role_validation @@ -61,9 +61,9 @@ async def get_account( return user -@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=UserUpdate) +@api_router.post("", dependencies=[Depends(bearer_schema)], response_model=AllUser) async def create_account( - user: UserUpdate, + user: UserCreate, connection: AsyncConnection = Depends(get_connection_dep), current_user=Depends(get_current_user), ): diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 4817122..52afb63 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -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 @@ -20,6 +20,17 @@ class UserUpdate(Base): status: Optional[AccountStatus] = None +class UserCreate(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 AllUser(Base): id: int name: str diff --git a/api/api/services/auth.py b/api/api/services/auth.py index 761949a..d4b0161 100644 --- a/api/api/services/auth.py +++ b/api/api/services/auth.py @@ -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 diff --git a/api/api/utils/hasher.py b/api/api/utils/hasher.py index 61d4c0e..3224245 100644 --- a/api/api/utils/hasher.py +++ b/api/api/utils/hasher.py @@ -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() diff --git a/api/api/utils/init.py b/api/api/utils/init.py index 06161a9..bc93be1 100644 --- a/api/api/utils/init.py +++ b/api/api/utils/init.py @@ -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, diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 1fef22c..aa4eb6d 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -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, UserUpdate } from '@/types/user'; +import { User, UserCreate } from '@/types/user'; const baseURL = `${import.meta.env.VITE_APP_HTTP_PROTOCOL}://${ import.meta.env.VITE_APP_API_URL @@ -115,10 +114,13 @@ const api = { return response.data; }, - async createUser(user: UserUpdate): Promise { + async createUser(user: UserCreate): Promise { const response = await base.post('/account', user); return response.data; }, + + // keyrings + async setPassword(userId: number, password: string): Promise {}, }; export default api; diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index a1cd8f8..d7f4097 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -14,7 +14,7 @@ import { import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useUserSelector } from '@/store/userStore'; -import { UserUpdate } from '@/types/user'; +import { UserCreate as NewUserCreate } from '@/types/user'; import { UserService } from '@/services/userService'; import { LoadingOutlined } from '@ant-design/icons'; @@ -55,9 +55,10 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => setFileList(newFileList); - const onFinish = async (values: UserUpdate) => { + const onFinish = async (values: NewUserCreate) => { setLoading(true); await UserService.createUser(values); + closeDrawer(); setLoading(false); message.info(t('createdAccountMessage'), 4); diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts index 725e8cd..c8a3172 100644 --- a/client/src/services/userService.ts +++ b/client/src/services/userService.ts @@ -1,5 +1,5 @@ import api from '@/api/api'; -import { AllUserResponse, User, UserUpdate } from '@/types/user'; +import { AllUserResponse, User, UserCreate } from '@/types/user'; export class UserService { static async getProfile(): Promise { @@ -24,7 +24,7 @@ export class UserService { return user; } - static async createUser(user: UserUpdate): Promise { + static async createUser(user: UserCreate): Promise { console.log('createUser'); const createdUser = api.createUser(user); return createdUser; From 7127d88524f9af8590746a996a0ad19b59bb4357 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 15:26:49 +0500 Subject: [PATCH 15/31] feat(client-Header): add userEdit on header --- client/src/components/ContentDrawer.tsx | 2 +- client/src/components/Header.tsx | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/client/src/components/ContentDrawer.tsx b/client/src/components/ContentDrawer.tsx index 678295d..4cf0627 100644 --- a/client/src/components/ContentDrawer.tsx +++ b/client/src/components/ContentDrawer.tsx @@ -11,7 +11,7 @@ interface ContentDrawerProps { type: 'create' | 'edit'; login?: string; name?: string; - email?: string; + email?: string | null; } export default function ContentDrawer({ diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index f640cb1..2872408 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -1,6 +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; @@ -8,6 +11,12 @@ 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 (
+ + {user?.id && } +
); } From 0eed0b0f20200be70f315fc61d5da9817db59b41 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 15:36:45 +0500 Subject: [PATCH 16/31] refactor(clint-UserEdit): remove login and role on self user --- client/src/components/UserEdit.tsx | 48 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/client/src/components/UserEdit.tsx b/client/src/components/UserEdit.tsx index 8bcef4e..14b430a 100644 --- a/client/src/components/UserEdit.tsx +++ b/client/src/components/UserEdit.tsx @@ -43,23 +43,25 @@ export default function UserEdit({ userId }: UserEditProps) { - - - + {user?.id === currentUser?.id ? undefined : ( + + + + )} @@ -83,19 +85,21 @@ export default function UserEdit({ userId }: UserEditProps) { - - - + {user?.id === currentUser?.id ? undefined : ( + + + + )} Date: Thu, 26 Jun 2025 16:15:03 +0500 Subject: [PATCH 17/31] feat(client): add userEdit --- api/api/endpoints/account.py | 4 +-- client/src/api/api.ts | 8 +++-- client/src/components/UserEdit.tsx | 58 ++++++++++++++++++++++++------ client/src/services/userService.ts | 8 ++++- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index f16d750..af68e4a 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -80,7 +80,7 @@ async def create_account( ) -@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, @@ -98,7 +98,7 @@ async def update_account( 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) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index aa4eb6d..714b26f 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -3,7 +3,7 @@ import axiosRetry from 'axios-retry'; import { Auth, Tokens } from '@/types/auth'; import { useAuthStore } from '@/store/authStore'; import { AuthService } from '@/services/authService'; -import { User, UserCreate } 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 @@ -119,8 +119,12 @@ const api = { return response.data; }, + async updateUser(userId: number, user: UserUpdate): Promise { + const response = await base.put(`/account/${userId}`, user); + return response.data; + }, + // keyrings - async setPassword(userId: number, password: string): Promise {}, }; export default api; diff --git a/client/src/components/UserEdit.tsx b/client/src/components/UserEdit.tsx index 14b430a..4b89020 100644 --- a/client/src/components/UserEdit.tsx +++ b/client/src/components/UserEdit.tsx @@ -1,7 +1,8 @@ import { UserService } from '@/services/userService'; import { useUserSelector } from '@/store/userStore'; -import { User } from '@/types/user'; -import { Button, Form, Input, Select } from 'antd'; +import { User, UserUpdate } from '@/types/user'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Button, Form, Input, Select, Spin } from 'antd'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,7 +16,18 @@ export default function UserEdit({ userId }: UserEditProps) { const currentUser = useUserSelector(); const [form] = Form.useForm(); const { t } = useTranslation(); - const [user, setUser] = useState(null); + const [user, setUser] = useState({ + id: 0, + name: '', + login: '', + email: '', + bindTenantId: '', + role: 'VIEWER', + meta: {}, + createdAt: '', + status: 'ACTIVE', + }); + const [loading, setLoading] = useState(false); useEffect(() => { async function getUser() { @@ -30,13 +42,30 @@ export default function UserEdit({ userId }: UserEditProps) { getUser(); }, []); + const onFinish = async (values: UserUpdate) => { + setLoading(true); + let updatedUser: Partial = {}; + + (Object.keys(values) as Array).forEach((key) => { + if (values[key] !== user[key]) { + updatedUser = { ...updatedUser, [key]: values[key] }; + } + }); + + if (Object.keys(updatedUser).length > 0) { + await UserService.updateUser(userId!, updatedUser); + } + + setLoading(false); + }; + return (
@@ -123,12 +152,21 @@ export default function UserEdit({ userId }: UserEditProps) { block style={{ color: '#000' }} > - save{' '} - {t('save')} + {loading ? ( + <> + } size="small">{' '} + {t('saving')} + + ) : ( + <> + save{' '} + {t('save')} + + )} diff --git a/client/src/services/userService.ts b/client/src/services/userService.ts index c8a3172..0bdb99f 100644 --- a/client/src/services/userService.ts +++ b/client/src/services/userService.ts @@ -1,5 +1,5 @@ import api from '@/api/api'; -import { AllUserResponse, User, UserCreate } from '@/types/user'; +import { AllUserResponse, User, UserCreate, UserUpdate } from '@/types/user'; export class UserService { static async getProfile(): Promise { @@ -29,4 +29,10 @@ export class UserService { const createdUser = api.createUser(user); return createdUser; } + + static async updateUser(userId: number, user: UserUpdate): Promise { + console.log('updateUser'); + const updatedUser = api.updateUser(userId, user); + return updatedUser; + } } From 3d8ee4835d4d2297586eca100be7ba07f2906c3a Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 16:38:26 +0500 Subject: [PATCH 18/31] refactor(db): increase account_keyring_table.key_value size --- api/api/db/alembic/versions/93106fbe7d83_.py | 38 ++++++++++++++++++++ api/api/db/tables/account.py | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 api/api/db/alembic/versions/93106fbe7d83_.py diff --git a/api/api/db/alembic/versions/93106fbe7d83_.py b/api/api/db/alembic/versions/93106fbe7d83_.py new file mode 100644 index 0000000..53d9604 --- /dev/null +++ b/api/api/db/alembic/versions/93106fbe7d83_.py @@ -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 ### diff --git a/api/api/db/tables/account.py b/api/api/db/tables/account.py index 84c689e..5325ddc 100644 --- a/api/api/db/tables/account.py +++ b/api/api/db/tables/account.py @@ -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), From edfd2c1159bfbbd8b6cfac324c407d475247df8b Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Thu, 26 Jun 2025 16:38:57 +0500 Subject: [PATCH 19/31] refactor(Makefile): fix alembic --- Makefile | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index f5ac720..1a7ff7a 100644 --- a/Makefile +++ b/Makefile @@ -20,21 +20,18 @@ start-client: migrate: cd api && \ - poetry env 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 && \ - poetry env activate && \ cd $(API_APPLICATION_NAME)/db && \ - PYTHONPATH='../..' alembic downgrade -1 + PYTHONPATH='../..' poetry run alembic downgrade -1 revision: cd api && \ - poetry env 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 && \ From 8f3fde623fe9f1cd00411caa673baad989ecb55e Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Fri, 27 Jun 2025 12:25:25 +0500 Subject: [PATCH 20/31] refactor(api): move create password --- api/api/db/logic/account.py | 4 +--- api/api/endpoints/account.py | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/api/db/logic/account.py b/api/api/db/logic/account.py index 78be803..2efb0af 100644 --- a/api/api/db/logic/account.py +++ b/api/api/db/logic/account.py @@ -6,10 +6,9 @@ from typing import Optional from sqlalchemy import func, insert, select from sqlalchemy.ext.asyncio import AsyncConnection -from api.db.logic.keyring import create_password_key from api.db.tables.account import account_table from api.schemas.account.account import User -from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate, UserUpdate +from api.schemas.endpoints.account import all_user_adapter, AllUser, AllUserResponse, UserCreate async def get_user_accaunt_page(connection: AsyncConnection, page, limit) -> Optional[AllUserResponse]: @@ -123,6 +122,5 @@ async def create_user(connection: AsyncConnection, user: UserCreate, creator_id: await connection.commit() new_user = await get_user_by_id(connection, res.lastrowid) - await create_password_key(connection, user.password, new_user.id) return new_user diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index af68e4a..9c34096 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -14,6 +14,7 @@ from api.db.logic.account import ( get_user_by_login, update_user_by_id, ) +from api.db.logic.keyring import create_password_key from api.db.tables.account import AccountStatus from api.schemas.account.account import User from api.schemas.base import bearer_schema @@ -73,6 +74,7 @@ async def create_account( 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( From 1eadd834e3e79ad5c46085745d7fc9025fed12ef Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Fri, 27 Jun 2025 13:25:25 +0500 Subject: [PATCH 21/31] feat(client): add update password --- client/src/components/Header.tsx | 2 +- client/src/components/UserEdit.tsx | 13 +++++++++---- client/src/config/i18n.ts | 2 ++ client/src/pages/AccountsPage.tsx | 11 ++++++++++- client/src/pages/LoginPage.tsx | 8 ++++++-- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 2872408..3a65726 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -70,7 +70,7 @@ export default function Header({ title, additionalContent }: HeaderProps) { closeDrawer={closeEditDrawer} type="edit" > - {user?.id && } + {user?.id && }
); diff --git a/client/src/components/UserEdit.tsx b/client/src/components/UserEdit.tsx index 4b89020..70740db 100644 --- a/client/src/components/UserEdit.tsx +++ b/client/src/components/UserEdit.tsx @@ -1,8 +1,8 @@ import { UserService } from '@/services/userService'; import { useUserSelector } from '@/store/userStore'; -import { User, UserUpdate } from '@/types/user'; +import { UserUpdate } from '@/types/user'; import { LoadingOutlined } from '@ant-design/icons'; -import { Button, Form, Input, Select, Spin } from 'antd'; +import { Button, Form, Input, message, Select, Spin } from 'antd'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,17 +10,19 @@ const { Option } = Select; interface UserEditProps { userId?: number; + closeDrawer: () => void; } -export default function UserEdit({ userId }: UserEditProps) { +export default function UserEdit({ userId, closeDrawer }: UserEditProps) { const currentUser = useUserSelector(); const [form] = Form.useForm(); const { t } = useTranslation(); - const [user, setUser] = useState({ + const [user, setUser] = useState({ id: 0, name: '', login: '', email: '', + password: '', bindTenantId: '', role: 'VIEWER', meta: {}, @@ -53,10 +55,13 @@ export default function UserEdit({ userId }: UserEditProps) { }); 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 ( diff --git a/client/src/config/i18n.ts b/client/src/config/i18n.ts index 360cde1..c3ea6eb 100644 --- a/client/src/config/i18n.ts +++ b/client/src/config/i18n.ts @@ -49,6 +49,7 @@ i18n createdAt: 'Created', saving: 'Saving...', createdAccountMessage: 'User successfully created!', + editAccountMessage: 'User successfully updated!', you: '(You)', }, }, @@ -91,6 +92,7 @@ i18n createdAt: 'Создано', saving: 'Сохранение...', createdAccountMessage: 'Пользователь успешно создан!', + editAccountMessage: 'Пользователь успешно обновлен!', you: '(Вы)', }, }, diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index 317cfee..7c65e2e 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -123,6 +123,15 @@ export default function AccountsPage() { title: t('createdAt'), dataIndex: 'createdAt', key: 'createdAt', + render: (text) => ( +
+ {new Date(text).toLocaleString('ru', { + year: '2-digit', + month: '2-digit', + day: '2-digit', + })} +
+ ), }, { title: t('status'), @@ -191,7 +200,7 @@ export default function AccountsPage() { closeDrawer={closeEditDrawer} type="edit" > - +
); diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index b6b65e9..c2c74c7 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -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() { /> -
+ Date: Fri, 27 Jun 2025 13:31:32 +0500 Subject: [PATCH 22/31] feat(api): add update password --- api/api/db/logic/keyring.py | 20 ++++++++++++++++++-- api/api/endpoints/account.py | 5 ++++- api/api/schemas/endpoints/account.py | 1 + client/src/types/openapi-types.ts | 4 +++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/api/api/db/logic/keyring.py b/api/api/db/logic/keyring.py index 4856d0a..e1b21b2 100644 --- a/api/api/db/logic/keyring.py +++ b/api/api/db/logic/keyring.py @@ -2,7 +2,7 @@ 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.ext.asyncio import AsyncConnection from api.db.tables.account import account_keyring_table, KeyStatus, KeyType @@ -80,7 +80,23 @@ async def create_password_key(connection: AsyncConnection, password: str | None, key_value=hasher.hash_data(password), created_at=datetime.now(timezone.utc), expiry=datetime.now(timezone.utc) + timedelta(days=365), - status=KeyStatus.ACTIVE.value, + status=KeyStatus.ACTIVE, ) 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() diff --git a/api/api/endpoints/account.py b/api/api/endpoints/account.py index 9c34096..5ce0da4 100644 --- a/api/api/endpoints/account.py +++ b/api/api/endpoints/account.py @@ -14,7 +14,7 @@ from api.db.logic.account import ( get_user_by_login, update_user_by_id, ) -from api.db.logic.keyring import create_password_key +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 @@ -95,6 +95,9 @@ 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: diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 52afb63..5ec16b0 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -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 diff --git a/client/src/types/openapi-types.ts b/client/src/types/openapi-types.ts index c203144..c9eebe6 100644 --- a/client/src/types/openapi-types.ts +++ b/client/src/types/openapi-types.ts @@ -280,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; @@ -534,7 +536,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["User"]; + "application/json": components["schemas"]["UserUpdate"]; }; }; /** @description Validation Error */ From ba65f366965b8d86b9cceccc79630e99550e348c Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Fri, 27 Jun 2025 16:41:48 +0500 Subject: [PATCH 23/31] feat(AccountsPage): add page and limit to search params --- client/src/pages/AccountsPage.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index 7c65e2e..0cda60b 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -8,10 +8,12 @@ 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 @@ -42,6 +44,10 @@ export default function AccountsPage() { async function getUsers() { const data = await UserService.getUsers(); setAccounts(data); + setSearchParams({ + page: data.currentPage.toString(), + limit: data.limit.toString(), + }); } getUsers(); @@ -152,6 +158,10 @@ export default function AccountsPage() { pagination.pageSize ).then((data) => { setAccounts(data); + setSearchParams({ + page: data.currentPage.toString(), + limit: data.limit.toString(), + }); }); }; From ad312d4ff8f7068e5988c0f1b9bf8bd38ee2c7a8 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Mon, 30 Jun 2025 12:19:29 +0500 Subject: [PATCH 24/31] feat(AccountsPage): loading with search params --- client/src/pages/AccountsPage.tsx | 123 +++++++++++++++--------------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index 0cda60b..b5a9963 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -1,19 +1,20 @@ -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'; +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(); + console.log("searchParams", searchParams); const [activeAccount, setActiveAccount] = useState< { login: string; id: number; name: string; email: string } | undefined @@ -45,8 +46,8 @@ export default function AccountsPage() { const data = await UserService.getUsers(); setAccounts(data); setSearchParams({ - page: data.currentPage.toString(), - limit: data.limit.toString(), + page: searchParams.get("page") || "1", + limit: searchParams.get("limit") || "10", }); } @@ -54,22 +55,22 @@ export default function AccountsPage() { }, []); const statusColor = { - ACTIVE: '#27AE60', - DISABLED: '#606060', - BLOCKED: '#FF0000', - DELETED: '#B30000', + ACTIVE: "#27AE60", + DISABLED: "#606060", + BLOCKED: "#FF0000", + DELETED: "#B30000", }; - const columns: TableProps['columns'] = [ + const columns: TableProps["columns"] = [ { - title: '#', - dataIndex: 'id', - key: 'id', + title: "#", + dataIndex: "id", + key: "id", }, { - title: t('nameLogin'), - dataIndex: 'nameLogin', - key: 'nameLogin', + title: t("nameLogin"), + dataIndex: "nameLogin", + key: "nameLogin", render: (text, record) => (
{ @@ -77,24 +78,24 @@ export default function AccountsPage() { login: record.login, id: record.id, name: record.name, - email: record.email || '', + email: record.email || "", }); showEditDrawer(); }} style={{ - display: 'flex', - alignItems: 'center', - gap: '16px', - cursor: 'pointer', + display: "flex", + alignItems: "center", + gap: "16px", + cursor: "pointer", }} >
-
+
{record.name}
-
{record.login}
+
{record.login}
), }, { - title: 'E-mail', - dataIndex: 'email', - key: 'email', + title: "E-mail", + dataIndex: "email", + key: "email", }, { - title: t('tenant'), - dataIndex: 'bindTenantId', - key: 'tenant', + title: t("tenant"), + dataIndex: "bindTenantId", + key: "tenant", }, { - title: t('role'), - dataIndex: 'role', - key: 'role', + title: t("role"), + dataIndex: "role", + key: "role", render: (text) =>
{t(text)}
, }, { - title: t('createdAt'), - dataIndex: 'createdAt', - key: 'createdAt', + title: t("createdAt"), + dataIndex: "createdAt", + key: "createdAt", render: (text) => (
- {new Date(text).toLocaleString('ru', { - year: '2-digit', - month: '2-digit', - day: '2-digit', + {new Date(text).toLocaleString("ru", { + year: "2-digit", + month: "2-digit", + day: "2-digit", })}
), }, { - title: t('status'), - dataIndex: 'status', - key: 'status', + title: t("status"), + dataIndex: "status", + key: "status", render: (text) => (
{t(text)} @@ -151,7 +152,7 @@ export default function AccountsPage() { }, ]; - const onTableChange: TableProps['onChange'] = (pagination) => { + const onTableChange: TableProps["onChange"] = (pagination) => { console.log(pagination); UserService.getUsers( pagination.current as number, @@ -168,15 +169,15 @@ export default function AccountsPage() { return ( <>
@@ -192,7 +193,7 @@ export default function AccountsPage() { current: accounts.currentPage, total: accounts.amountCount, }} - rowKey={'id'} + rowKey={"id"} /> Date: Mon, 30 Jun 2025 12:37:45 +0500 Subject: [PATCH 25/31] feat(AccountsPage): update accounts list after create user --- client/src/components/UserCreate.tsx | 159 ++++++++++++++------------- client/src/pages/AccountsPage.tsx | 29 +++-- 2 files changed, 101 insertions(+), 87 deletions(-) diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index d7f4097..99cdb9b 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -10,17 +10,18 @@ import { 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'; +} from "antd"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useUserSelector } from "@/store/userStore"; +import { AllUserResponse, UserCreate as NewUserCreate } from "@/types/user"; +import { UserService } from "@/services/userService"; +import { LoadingOutlined } from "@ant-design/icons"; +import { useSearchParams } from "react-router-dom"; const { Option } = Select; -type FileType = Parameters>[0]; +type FileType = Parameters>[0]; const getBase64 = (file: FileType): Promise => new Promise((resolve, reject) => { @@ -32,13 +33,19 @@ const getBase64 = (file: FileType): Promise => interface UserCreateProps { closeDrawer: () => void; + setAccounts: React.Dispatch>; + getUsers: () => Promise; } -export default function UserCreate({ closeDrawer }: UserCreateProps) { +export default function UserCreate({ + closeDrawer, + setAccounts, + 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([]); @@ -52,49 +59,49 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { 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); + message.info(t("createdAccountMessage"), 4); }; const customUploadButton = (
add_photo_alternate
- - {t('selectPhoto')} + + {t("selectPhoto")}
); const photoToUpload = ( -
+
{previewImage && ( setPreviewOpen(visible), - afterOpenChange: (visible) => !visible && setPreviewImage(''), + afterOpenChange: (visible) => !visible && setPreviewImage(""), }} src={previewImage} /> @@ -121,24 +128,24 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { return (
{photoToUpload} @@ -149,83 +156,83 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { layout="vertical" onFinish={onFinish} initialValues={{ - name: '', - login: '', - password: '', - email: '', - bindTenantId: '', - role: '', - status: '', + name: "", + login: "", + password: "", + email: "", + bindTenantId: "", + role: "", + status: "", }} - style={{ flex: 1, display: 'flex', flexDirection: 'column' }} + style={{ flex: 1, display: "flex", flexDirection: "column" }} > - + {user && user.role === "OWNER" ? ( + ) : undefined} - - + + - + + + + @@ -236,21 +243,21 @@ export default function UserCreate({ closeDrawer }: UserCreateProps) { type="primary" htmlType="submit" block - style={{ color: '#000' }} + style={{ color: "#000" }} > {loading ? ( <> - } size="small">{' '} - {t('saving')} + } size="small">{" "} + {t("saving")} ) : ( <> save{' '} - {t('addAccount')} + style={{ height: "18px", width: "18px" }} + />{" "} + {t("addAccount")} )} diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index b5a9963..bc289a3 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -14,7 +14,6 @@ export default function AccountsPage() { const { t } = useTranslation(); const [openCreate, setOpenCreate] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); - console.log("searchParams", searchParams); const [activeAccount, setActiveAccount] = useState< { login: string; id: number; name: string; email: string } | undefined @@ -41,16 +40,20 @@ export default function AccountsPage() { limit: 10, }); - useEffect(() => { - async function getUsers() { - const data = await UserService.getUsers(); - setAccounts(data); - setSearchParams({ - page: searchParams.get("page") || "1", - limit: searchParams.get("limit") || "10", - }); - } + async function getUsers() { + setSearchParams({ + page: searchParams.get("page") || "1", + limit: searchParams.get("limit") || "10", + }); + const data = await UserService.getUsers( + Number(searchParams.get("page")), + Number(searchParams.get("limit")) + ); + console.log("searchParams", searchParams); + setAccounts(data); + } + useEffect(() => { getUsers(); }, []); @@ -201,7 +204,11 @@ export default function AccountsPage() { closeDrawer={closeCreateDrawer} type="create" > - + Date: Mon, 30 Jun 2025 14:02:53 +0500 Subject: [PATCH 26/31] fix(AccountPage): fix empty string in search params --- client/src/pages/AccountsPage.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index bc289a3..0b229ed 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -41,14 +41,13 @@ export default function AccountsPage() { }); async function getUsers() { + const page = Number(searchParams.get("page") || "1"); + const limit = Number(searchParams.get("limit") || "10"); setSearchParams({ - page: searchParams.get("page") || "1", - limit: searchParams.get("limit") || "10", + page: page.toString(), + limit: limit.toString(), }); - const data = await UserService.getUsers( - Number(searchParams.get("page")), - Number(searchParams.get("limit")) - ); + const data = await UserService.getUsers(page, limit); console.log("searchParams", searchParams); setAccounts(data); } From ad1369c3e31e6036daf27c2758614defa6551aec Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Mon, 30 Jun 2025 16:30:53 +0500 Subject: [PATCH 27/31] refactor(api): change UserCreate model fields --- api/api/schemas/endpoints/account.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/api/schemas/endpoints/account.py b/api/api/schemas/endpoints/account.py index 5ec16b0..60a29ff 100644 --- a/api/api/schemas/endpoints/account.py +++ b/api/api/schemas/endpoints/account.py @@ -22,14 +22,14 @@ class UserUpdate(Base): class UserCreate(Base): - name: Optional[str] = Field(None, max_length=100) - login: Optional[str] = Field(None, max_length=100) + 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: Optional[AccountRole] = None + role: AccountRole meta: Optional[dict] = None - status: Optional[AccountStatus] = None + status: AccountStatus class AllUser(Base): From 4c0beb24f9a04b4d2281c5b80340e0de4d84e1ce Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Mon, 30 Jun 2025 17:52:31 +0500 Subject: [PATCH 28/31] fix(api): on duplicate password update --- api/api/db/logic/keyring.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/api/db/logic/keyring.py b/api/api/db/logic/keyring.py index e1b21b2..74c19ba 100644 --- a/api/api/db/logic/keyring.py +++ b/api/api/db/logic/keyring.py @@ -3,12 +3,12 @@ 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 api.db.tables.account import account_keyring_table, KeyStatus, KeyType from api.schemas.account.account_keyring import AccountKeyring from api.utils.hasher import hasher -from api.utils.key_id_gen import KeyIdGenerator async def get_key_by_id(connection: AsyncConnection, key_id: str) -> Optional[AccountKeyring]: @@ -73,15 +73,19 @@ async def create_key(connection: AsyncConnection, key: AccountKeyring, key_id: i async def create_password_key(connection: AsyncConnection, password: str | None, owner_id: int): if password is None: password = hasher.generate_password() - stmt = insert(account_keyring_table).values( + hashed_password = hasher.hash_data(password) + stmt = mysql_insert(account_keyring_table).values( owner_id=owner_id, key_type=KeyType.PASSWORD.value, - key_id=KeyIdGenerator(), - key_value=hasher.hash_data(password), + 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() From a9365001015383137be7611f0077cf7b8b0a4253 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Tue, 1 Jul 2025 13:35:40 +0500 Subject: [PATCH 29/31] refactor(client): remove required fileds --- client/src/components/UserCreate.tsx | 16 +++++----------- client/src/pages/AccountsPage.tsx | 6 +----- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/client/src/components/UserCreate.tsx b/client/src/components/UserCreate.tsx index 99cdb9b..81393d3 100644 --- a/client/src/components/UserCreate.tsx +++ b/client/src/components/UserCreate.tsx @@ -14,10 +14,9 @@ import { import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useUserSelector } from "@/store/userStore"; -import { AllUserResponse, UserCreate as NewUserCreate } from "@/types/user"; +import { UserCreate as NewUserCreate } from "@/types/user"; import { UserService } from "@/services/userService"; import { LoadingOutlined } from "@ant-design/icons"; -import { useSearchParams } from "react-router-dom"; const { Option } = Select; @@ -33,15 +32,10 @@ const getBase64 = (file: FileType): Promise => interface UserCreateProps { closeDrawer: () => void; - setAccounts: React.Dispatch>; getUsers: () => Promise; } -export default function UserCreate({ - closeDrawer, - setAccounts, - getUsers, -}: UserCreateProps) { +export default function UserCreate({ closeDrawer, getUsers }: UserCreateProps) { const user = useUserSelector(); const { t } = useTranslation(); const [previewOpen, setPreviewOpen] = useState(false); @@ -185,7 +179,7 @@ export default function UserCreate({ @@ -194,7 +188,7 @@ export default function UserCreate({ label={t("email")} name="email" rules={[ - { required: true, message: t("emailMessage") }, + { message: t("emailMessage") }, { type: "email", message: t("emailErrorMessage") }, ]} > @@ -204,7 +198,7 @@ export default function UserCreate({ diff --git a/client/src/pages/AccountsPage.tsx b/client/src/pages/AccountsPage.tsx index 0b229ed..19e53a0 100644 --- a/client/src/pages/AccountsPage.tsx +++ b/client/src/pages/AccountsPage.tsx @@ -203,11 +203,7 @@ export default function AccountsPage() { closeDrawer={closeCreateDrawer} type="create" > - + Date: Wed, 2 Jul 2025 12:23:22 +0500 Subject: [PATCH 30/31] refactor(api): format with ruff --- api/api/db/logic/keyring.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/api/db/logic/keyring.py b/api/api/db/logic/keyring.py index 74c19ba..ad3de1b 100644 --- a/api/api/db/logic/keyring.py +++ b/api/api/db/logic/keyring.py @@ -83,9 +83,7 @@ async def create_password_key(connection: AsyncConnection, password: str | None, expiry=datetime.now(timezone.utc) + timedelta(days=365), status=KeyStatus.ACTIVE, ) - stmt.on_duplicate_key_update( - key_value=hashed_password - ) + stmt.on_duplicate_key_update(key_value=hashed_password) await connection.execute(stmt) await connection.commit() From e6589a09503ddf890e5ae3c30bdc80bd3c09e5e0 Mon Sep 17 00:00:00 2001 From: Vladislav Syrochkin Date: Wed, 2 Jul 2025 12:25:11 +0500 Subject: [PATCH 31/31] chore: update client and api patch version to 0.0.5 --- api/pyproject.toml | 2 +- client/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 6e82317..7652347 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api" -version = "0.0.4" +version = "0.0.5" description = "" authors = [{ name = "Vladislav", email = "vlad.dev@heado.ru" }] readme = "README.md" diff --git a/client/package.json b/client/package.json index dff2617..516b19e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "0.0.4", + "version": "0.0.5", "private": true, "dependencies": { "@ant-design/icons": "^5.6.1",