Compare commits

..

2 Commits

5 changed files with 438 additions and 1 deletions

View File

@ -17,13 +17,15 @@ import {
Lock as KeyIcon, Lock as KeyIcon,
Visibility as EyeIcon, Visibility as EyeIcon,
VisibilityOff as EyeOffIcon, VisibilityOff as EyeOffIcon,
Notifications as BellIcon Notifications as BellIcon,
IntegrationInstructions as IntegrationIcon
} from "@mui/icons-material"; } from "@mui/icons-material";
import AccountProfile from "../../components/AccountProfile"; import AccountProfile from "../../components/AccountProfile";
import AccountSecurity from "../../components/AccountSecurity"; import AccountSecurity from "../../components/AccountSecurity";
import AccountNotifications from "../../components/AccountNotifications"; import AccountNotifications from "../../components/AccountNotifications";
import AccountAgentTransactionSection from "../../components/AccountAgentTransactionSection"; import AccountAgentTransactionSection from "../../components/AccountAgentTransactionSection";
import TabsNav from "../../components/TabsNav"; import TabsNav from "../../components/TabsNav";
import AccountIntegration from "../../components/AccountIntegration";
const initialNotifications = { const initialNotifications = {
emailNotifications: true, emailNotifications: true,
@ -48,6 +50,7 @@ export default function AccountPage() {
{ id: "security", label: "Безопасность", icon: <KeyIcon fontSize="small" /> }, { id: "security", label: "Безопасность", icon: <KeyIcon fontSize="small" /> },
{ id: "notifications", label: "Уведомления", icon: <BellIcon fontSize="small" /> }, { id: "notifications", label: "Уведомления", icon: <BellIcon fontSize="small" /> },
{ id: "agent-transactions", label: "Транзакции агентов", icon: <WorkIcon fontSize="small" /> }, { id: "agent-transactions", label: "Транзакции агентов", icon: <WorkIcon fontSize="small" /> },
{ id: "integration", label: "Интеграции", icon: <IntegrationIcon fontSize="small" /> },
]; ];
return ( return (
@ -69,6 +72,9 @@ export default function AccountPage() {
{activeTab === "agent-transactions" && ( {activeTab === "agent-transactions" && (
<AccountAgentTransactionSection /> <AccountAgentTransactionSection />
)} )}
{activeTab === "integration" && (
<AccountIntegration />
)}
</div> </div>
</AuthGuard> </AuthGuard>
); );

View File

@ -0,0 +1,208 @@
"use client";
import React, { useState, useMemo, useEffect } from "react";
import {
Box,
Button,
Typography,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton
} from "@mui/material";
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 {
// description: string;
// masked_token: string;
// rawToken?: string;
// create_dttm: string;
// use_dttm?: string;
// }
const AccountIntegration = () => {
const [tokens, setTokens] = useState<Token[]>([]);
const [openCreateDialog, setOpenCreateDialog] = useState(false);
const [showTokenCreatedSuccess, setShowTokenCreatedSuccess] = useState(false);
const [createdRawToken, setCreatedRawToken] = useState<string | null>(null);
const [openEditDialog, setOpenEditDialog] = useState(false);
const [editingToken, setEditingToken] = useState<Token | null>(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);
setCreatedRawToken(null);
};
const handleCloseCreateDialog = () => {
setOpenCreateDialog(false);
};
const handleOpenEditDialog = (token: Token) => {
setEditingToken(token);
setOpenEditDialog(true);
};
const handleCloseEditDialog = () => {
setOpenEditDialog(false);
setEditingToken(null);
};
const handleTokenGenerate = async (description: 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", {
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 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 (
<Box sx={{ padding: 2 }}>
<Typography variant="h5" gutterBottom>
Управление токенами интеграции
</Typography>
<IntegrationTokensTable
tokens={tokens}
onOpenCreateDialog={handleOpenCreateDialog}
onOpenEditDialog={handleOpenEditDialog}
onDeleteToken={handleDeleteToken}
/>
<CreateTokenDialog
open={openCreateDialog}
onClose={handleCloseCreateDialog}
onTokenGenerate={handleTokenGenerate}
generatedToken={createdRawToken}
/>
<CreateTokenDialog
open={openEditDialog}
onClose={handleCloseEditDialog}
isEditMode={true}
initialDescription={editingToken?.description || ""}
editingTokenId={editingToken?.id || 0}
onTokenUpdate={handleTokenUpdate}
/>
</Box>
);
};
export default AccountIntegration;

View File

@ -0,0 +1,120 @@
"use client";
import React, { useState, useEffect } from "react";
import {
Box,
Button,
TextField,
Typography,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import styles from "../styles/account.module.css";
import { Token } from "../types/tokens";
interface CreateTokenDialogProps {
open: boolean;
onClose: () => void;
onTokenGenerate?: (description: string) => Promise<void>;
isEditMode?: boolean;
initialDescription?: string;
editingTokenId?: number;
onTokenUpdate?: (id: number, description: string) => void;
generatedToken?: string | null;
}
const CreateTokenDialog: React.FC<CreateTokenDialogProps> = ({
open,
onClose,
onTokenGenerate,
isEditMode = false,
initialDescription = "",
onTokenUpdate,
generatedToken,
editingTokenId,
}) => {
const [description, setDescription] = useState(initialDescription);
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
if (open) {
setDescription(initialDescription);
setShowWarning(false);
}
}, [open, initialDescription]);
const handleGenerateClick = async () => {
if (description.trim() === "") {
setShowWarning(true);
return;
}
await onTokenGenerate?.(description);
};
const handleUpdateClick = () => {
if (description.trim() === "") {
setShowWarning(true);
return;
}
console.log("Attempting to update token. editingTokenId:", editingTokenId, "Description:", description);
if (onTokenUpdate && typeof editingTokenId === 'number') {
onTokenUpdate(editingTokenId, description);
onClose();
} else {
console.error("Cannot update token: onTokenUpdate is missing or editingTokenId is invalid.", {onTokenUpdateExists: !!onTokenUpdate, editingTokenIdValue: editingTokenId});
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{isEditMode ? "Редактировать токен" : "Создать новый токен"}</DialogTitle>
<DialogContent>
{showWarning && (
<Typography color="error" variant="body2" sx={{ mb: 2 }}>
Пожалуйста, введите описание токена.
</Typography>
)}
<TextField
autoFocus
margin="dense"
label="Описание токена"
type="text"
fullWidth
variant="outlined"
value={description}
onChange={(e) => setDescription(e.target.value)}
error={showWarning}
helperText={showWarning ? "Описание не может быть пустым" : ""}
/>
{!isEditMode && generatedToken && (
<Box sx={{ mt: 2, p: 2, border: '1px solid #ccc', borderRadius: '4px', backgroundColor: '#f0f0f0'}}>
<Typography variant="subtitle2">Созданный токен:</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px'}}>
<Typography variant="body2" sx={{ wordBreak: 'break-all'}}>
{generatedToken}
</Typography>
</Box>
<Typography variant="caption" color="textSecondary">
Скопируйте этот токен. Он будет виден только сейчас.
</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
{!generatedToken && <Button onClick={onClose}>Отмена</Button>}
{isEditMode ? (
<Button onClick={handleUpdateClick} variant="contained">
Сохранить
</Button>
) : (
<Button onClick={generatedToken ? onClose : handleGenerateClick} variant="contained">
{generatedToken ? "ОК" : "Создать"}
</Button>
)}
</DialogActions>
</Dialog>
);
};
export default CreateTokenDialog;

View File

@ -0,0 +1,95 @@
"use client";
import React, { useMemo } from "react";
import {
Box,
Button,
Typography,
IconButton
} 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";
import { Token } from "../types/tokens";
interface IntegrationTokensTableProps {
tokens: Token[];
onOpenCreateDialog: () => void;
onOpenEditDialog: (token: Token) => void;
onDeleteToken: (id: number) => void;
}
const IntegrationTokensTable: React.FC<IntegrationTokensTableProps> = ({
tokens,
onOpenCreateDialog,
onOpenEditDialog,
onDeleteToken,
}) => {
const columns = useMemo<MRT_ColumnDef<Token>[]>(
() => [
{
accessorKey: "description",
header: "Описание",
size: 200,
},
{
accessorKey: "masked_token",
header: "Токен",
size: 250,
Cell: ({ cell }) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px'}}>
<Typography variant="body2">
{cell.getValue<string>()}
</Typography>
</Box>
)
},
{
accessorKey: "create_dttm",
header: "Дата создания",
size: 150,
Cell: ({ cell }) => new Date(cell.getValue<string>()).toLocaleString(),
},
{
accessorKey: "use_dttm",
header: "Дата последнего использования",
size: 200,
Cell: ({ cell }) => cell.getValue() ? new Date(cell.getValue<string>()).toLocaleString() : "Никогда",
},
],
[],
);
const table = useMaterialReactTable({
columns,
data: tokens,
enableRowActions: true,
positionActionsColumn: "last",
renderRowActions: ({ row }) => (
<Box sx={{ display: "flex", flexWrap: "nowrap", gap: "8px" }}>
<IconButton
color="primary"
onClick={() => onOpenEditDialog(row.original)}
>
<EditIcon />
</IconButton>
<IconButton
color="error"
onClick={() => onDeleteToken(row.original.id)}
>
<DeleteIcon />
</IconButton>
</Box>
),
renderTopToolbarCustomActions: () => (
<Button
onClick={onOpenCreateDialog}
variant="contained"
>
Создать новый токен
</Button>
),
});
return <MaterialReactTable table={table} />;
};
export default IntegrationTokensTable;

8
src/types/tokens.ts Normal file
View File

@ -0,0 +1,8 @@
export interface Token {
id: number;
description: string;
masked_token: string;
rawToken?: string;
create_dttm: string;
use_dttm?: string;
}