From 5380866af3ac2dc9b882229fe57681228ce5bb07 Mon Sep 17 00:00:00 2001 From: Redsandyg Date: Mon, 9 Jun 2025 15:28:01 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20AccountIntegration=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F,=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F,=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=D0=B7=20=D0=BA=D1=83=D0=BA=D0=B8.=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=85=20CreateTokenDialog=20=D0=B8=20IntegrationTokensTa?= =?UTF-8?q?ble.=20=D0=92=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D1=82=D0=B8=D0=BF=20Token=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AccountIntegration.tsx | 169 ++++++++++++++++------ src/components/CreateTokenDialog.tsx | 169 +++++++++------------- src/components/IntegrationTokensTable.tsx | 104 +++++-------- src/types/tokens.ts | 8 + 4 files changed, 237 insertions(+), 213 deletions(-) create mode 100644 src/types/tokens.ts diff --git a/src/components/AccountIntegration.tsx b/src/components/AccountIntegration.tsx index d1f1d04..70f1b9d 100644 --- a/src/components/AccountIntegration.tsx +++ b/src/components/AccountIntegration.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Box, Button, @@ -14,29 +14,57 @@ import { import CreateTokenDialog from "./CreateTokenDialog"; import IntegrationTokensTable from "./IntegrationTokensTable"; import styles from "../styles/account.module.css"; +import Cookies from "js-cookie"; +import { Token } from "../types/tokens"; // Компонент для управления интеграциями и токенами -interface Token { - id: string; - description: string; - token: string; - createdAt: string; - lastUsedAt?: string; -} +// interface Token { +// description: string; +// masked_token: string; +// rawToken?: string; +// create_dttm: string; +// use_dttm?: string; +// } const AccountIntegration = () => { const [tokens, setTokens] = useState([]); const [openCreateDialog, setOpenCreateDialog] = useState(false); - const [tokenDescription, setTokenDescription] = useState(""); - const [showTokenWarning, setShowTokenWarning] = useState(false); const [showTokenCreatedSuccess, setShowTokenCreatedSuccess] = useState(false); + const [createdRawToken, setCreatedRawToken] = useState(null); const [openEditDialog, setOpenEditDialog] = useState(false); const [editingToken, setEditingToken] = useState(null); + const fetchTokens = async () => { + try { + const token = Cookies.get("access_token"); + if (!token) { + console.error("Access token not found"); + return; + } + const res = await fetch("/api/account/integration-tokens", { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + }); + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + const data: Token[] = await res.json(); + setTokens(data); + } catch (error) { + console.error("Failed to fetch tokens:", error); + } + }; + + useEffect(() => { + fetchTokens(); + }, []); + const handleOpenCreateDialog = () => { setOpenCreateDialog(true); - setTokenDescription(""); - setShowTokenWarning(false); + setCreatedRawToken(null); }; const handleCloseCreateDialog = () => { @@ -53,33 +81,96 @@ const AccountIntegration = () => { setEditingToken(null); }; - const handleTokenGenerate = (description: string, newTokenValue: string) => { - const newToken: Token = { - id: (tokens.length + 1).toString(), - description: description, - token: newTokenValue, - createdAt: new Date().toLocaleString(), - lastUsedAt: "Никогда", - }; - setTokens([...tokens, newToken]); - setShowTokenCreatedSuccess(true); - setTimeout(() => setShowTokenCreatedSuccess(false), 1500); - }; + const handleTokenGenerate = async (description: string) => { + try { + const authToken = Cookies.get("access_token"); + if (!authToken) { + console.error("Access token not found"); + return; + } - const handleTokenUpdate = (updatedDescription: string) => { - if (editingToken) { - setTokens(prevTokens => - prevTokens.map(token => - token.id === editingToken.id ? { ...token, description: updatedDescription } : token - ) - ); - setOpenEditDialog(false); - setEditingToken(null); + const res = await fetch("/api/account/integration-tokens", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${authToken}` + }, + body: JSON.stringify({ description: description }) + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.detail || "Ошибка генерации токена"); + } + + const newTokenData: Token = await res.json(); + + await fetchTokens(); + + setCreatedRawToken(newTokenData.rawToken || null); + setShowTokenCreatedSuccess(true); + setTimeout(() => setShowTokenCreatedSuccess(false), 3000); + } catch (error: any) { + console.error("Ошибка при генерации токена:", error.message); } }; - const handleDeleteToken = (tokenId: string) => { - setTokens(prevTokens => prevTokens.filter(token => token.id !== tokenId)); + const handleTokenUpdate = async (id: number, updatedDescription: string) => { + try { + const authToken = Cookies.get("access_token"); + if (!authToken) { + console.error("Access token not found"); + return; + } + + const res = await fetch("/api/account/integration-tokens/update-description", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${authToken}` + }, + body: JSON.stringify({ id: id, description: updatedDescription }) + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.detail || "Ошибка обновления описания токена"); + } + + // После успешного обновления, обновим список токенов + await fetchTokens(); + setOpenEditDialog(false); + setEditingToken(null); + } catch (error: any) { + console.error("Ошибка при обновлении токена:", error.message); + } + }; + + const handleDeleteToken = async (id: number) => { + try { + const authToken = Cookies.get("access_token"); + if (!authToken) { + console.error("Access token not found"); + return; + } + + const res = await fetch(`/api/account/integration-tokens/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${authToken}` + }, + }); + + if (!res.ok) { + const errorData = await res.json(); + throw new Error(errorData.detail || "Ошибка удаления токена"); + } + + await fetchTokens(); + } catch (error: any) { + console.error("Ошибка при удалении токена:", error.message); + } }; return ( @@ -99,6 +190,7 @@ const AccountIntegration = () => { open={openCreateDialog} onClose={handleCloseCreateDialog} onTokenGenerate={handleTokenGenerate} + generatedToken={createdRawToken} /> { onClose={handleCloseEditDialog} isEditMode={true} initialDescription={editingToken?.description || ""} + editingTokenId={editingToken?.id || 0} onTokenUpdate={handleTokenUpdate} /> - - {showTokenCreatedSuccess && ( -
- Токен успешно создан! -
- )} ); }; diff --git a/src/components/CreateTokenDialog.tsx b/src/components/CreateTokenDialog.tsx index 925d39e..4992400 100644 --- a/src/components/CreateTokenDialog.tsx +++ b/src/components/CreateTokenDialog.tsx @@ -8,147 +8,110 @@ import { Dialog, DialogActions, DialogContent, - DialogContentText, DialogTitle, - Snackbar, - Alert, } from "@mui/material"; -import { ContentCopy as ContentCopyIcon } from "@mui/icons-material"; import styles from "../styles/account.module.css"; +import { Token } from "../types/tokens"; interface CreateTokenDialogProps { open: boolean; onClose: () => void; - onTokenGenerate?: (description: string, token: string) => void; + onTokenGenerate?: (description: string) => Promise; isEditMode?: boolean; initialDescription?: string; - onTokenUpdate?: (updatedDescription: string) => void; + editingTokenId?: number; + onTokenUpdate?: (id: number, description: string) => void; + generatedToken?: string | null; } const CreateTokenDialog: React.FC = ({ open, onClose, onTokenGenerate, - isEditMode, - initialDescription, + isEditMode = false, + initialDescription = "", onTokenUpdate, + generatedToken, + editingTokenId, }) => { - const [tokenDescription, setTokenDescription] = useState(""); - const [generatedToken, setGeneratedToken] = useState(""); - const [showTokenWarning, setShowTokenWarning] = useState(false); - const [showCopySuccess, setShowCopySuccess] = useState(false); + const [description, setDescription] = useState(initialDescription); + const [showWarning, setShowWarning] = useState(false); useEffect(() => { - if (!open) { - setTokenDescription(""); - setGeneratedToken(""); - setShowTokenWarning(false); - setShowCopySuccess(false); - } else if (isEditMode && initialDescription) { - setTokenDescription(initialDescription); - } else { - setTokenDescription(""); - setGeneratedToken(""); - setShowTokenWarning(false); - setShowCopySuccess(false); + if (open) { + setDescription(initialDescription); + setShowWarning(false); } - }, [open, isEditMode, initialDescription]); + }, [open, initialDescription]); - const handleGenerateToken = () => { - if (tokenDescription.trim() === "") return; - - const newTokenValue = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - setGeneratedToken(newTokenValue); - setShowTokenWarning(true); - if (onTokenGenerate) { - onTokenGenerate(tokenDescription, newTokenValue); + const handleGenerateClick = async () => { + if (description.trim() === "") { + setShowWarning(true); + return; } + await onTokenGenerate?.(description); }; - const handleUpdateDescription = () => { - if (tokenDescription.trim() === "") return; - if (onTokenUpdate) { - onTokenUpdate(tokenDescription); + const handleUpdateClick = () => { + if (description.trim() === "") { + setShowWarning(true); + return; } - onClose(); - }; - - const handleCopy = () => { - if (!generatedToken) return; - if (typeof navigator !== "undefined" && navigator.clipboard) { - navigator.clipboard.writeText(generatedToken); + console.log("Attempting to update token. editingTokenId:", editingTokenId, "Description:", description); + if (onTokenUpdate && typeof editingTokenId === 'number') { + onTokenUpdate(editingTokenId, description); + onClose(); } else { - const textarea = document.createElement("textarea"); - textarea.value = generatedToken; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand("copy"); - document.body.removeChild(textarea); + console.error("Cannot update token: onTokenUpdate is missing or editingTokenId is invalid.", {onTokenUpdateExists: !!onTokenUpdate, editingTokenIdValue: editingTokenId}); } - setShowCopySuccess(true); - setTimeout(() => setShowCopySuccess(false), 1500); }; return ( - - {isEditMode ? "Редактировать описание токена" : "Создать новый токен"} + + {isEditMode ? "Редактировать токен" : "Создать новый токен"} - {isEditMode || !generatedToken ? ( - <> - - {isEditMode ? "Введите новое описание для токена." : "Пожалуйста, введите описание для вашего нового токена."} - - setTokenDescription(e.target.value)} - /> - - ) : ( - - Ваш новый токен: - - + {showWarning && ( + + Пожалуйста, введите описание токена. + + )} + setDescription(e.target.value)} + error={showWarning} + helperText={showWarning ? "Описание не может быть пустым" : ""} + /> + {!isEditMode && generatedToken && ( + + Созданный токен: + + {generatedToken} - - {showTokenWarning && ( - - Внимание: Этот токен не может быть восстановлен. Пожалуйста, скопируйте его сейчас! - - )} - {showCopySuccess && ( -
- Токен скопирован! -
- )} + + Скопируйте этот токен. Он будет виден только сейчас. +
)}
- {!(isEditMode || generatedToken) ? ( - - ) : null} - + {!generatedToken && } + {isEditMode ? ( + + ) : ( + + )}
); diff --git a/src/components/IntegrationTokensTable.tsx b/src/components/IntegrationTokensTable.tsx index 666ca81..68cf173 100644 --- a/src/components/IntegrationTokensTable.tsx +++ b/src/components/IntegrationTokensTable.tsx @@ -8,20 +8,13 @@ import { } from "@mui/material"; import { MaterialReactTable, type MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from "@mui/icons-material"; - -interface Token { - id: string; - description: string; - token: string; - createdAt: string; - lastUsedAt?: string; -} +import { Token } from "../types/tokens"; interface IntegrationTokensTableProps { tokens: Token[]; onOpenCreateDialog: () => void; onOpenEditDialog: (token: Token) => void; - onDeleteToken: (tokenId: string) => void; + onDeleteToken: (id: number) => void; } const IntegrationTokensTable: React.FC = ({ @@ -36,87 +29,60 @@ const IntegrationTokensTable: React.FC = ({ accessorKey: "description", header: "Описание", size: 200, - Cell: ({ renderedCellValue }) => renderedCellValue, }, { - accessorKey: "token", + accessorKey: "masked_token", header: "Токен", size: 250, - Cell: ({ renderedCellValue }) => { - const tokenValue = renderedCellValue as string; - const maskedToken = tokenValue.substring(0, 5) + "***********************" + tokenValue.substring(tokenValue.length - 4); - return ( - - {maskedToken} + Cell: ({ cell }) => ( + + + {cell.getValue()} - ); - }, + + ) }, { - accessorKey: "createdAt", + accessorKey: "create_dttm", header: "Дата создания", size: 150, - Cell: ({ renderedCellValue }) => renderedCellValue, + Cell: ({ cell }) => new Date(cell.getValue()).toLocaleString(), }, { - accessorKey: "lastUsedAt", - header: "Последнее использование", - size: 150, - Cell: ({ renderedCellValue }) => renderedCellValue, - }, - { - id: 'actions', - header: 'Действия', - size: 100, - Cell: ({ row }) => ( - - onOpenEditDialog(row.original)} - color="primary" - > - - - onDeleteToken(row.original.id)} - color="error" - > - - - - ), + accessorKey: "use_dttm", + header: "Дата последнего использования", + size: 200, + Cell: ({ cell }) => cell.getValue() ? new Date(cell.getValue()).toLocaleString() : "Никогда", }, ], - [onOpenEditDialog, onDeleteToken], + [], ); const table = useMaterialReactTable({ columns, data: tokens, - enableColumnActions: false, - enableColumnFilters: true, - enablePagination: true, - enableSorting: true, - enableBottomToolbar: true, - enableTopToolbar: true, - enableDensityToggle: true, - enableGlobalFilter: true, - enableHiding: true, - renderEmptyRowsFallback: () => ( - - - У вас пока нет созданных токенов. - + enableRowActions: true, + positionActionsColumn: "last", + renderRowActions: ({ row }) => ( + + onOpenEditDialog(row.original)} + > + + + onDeleteToken(row.original.id)} + > + + ), - muiTableBodyCellProps: { sx: { fontSize: 14 } }, - muiTableHeadCellProps: { sx: { fontWeight: 700 } }, - initialState: { pagination: { pageSize: 10, pageIndex: 0 } }, renderTopToolbarCustomActions: () => ( - diff --git a/src/types/tokens.ts b/src/types/tokens.ts new file mode 100644 index 0000000..37d6de7 --- /dev/null +++ b/src/types/tokens.ts @@ -0,0 +1,8 @@ +export interface Token { + id: number; + description: string; + masked_token: string; + rawToken?: string; + create_dttm: string; + use_dttm?: string; +} \ No newline at end of file