From ed439656f87d08f0ffc4e729e036a62fef63cf14 Mon Sep 17 00:00:00 2001 From: NikitaTorbenko <2015nekitciti@gmail.com> Date: Sat, 4 Apr 2026 05:52:43 +0300 Subject: [PATCH] feat: add registration token --- .../layout/navigation/navigation.tsx | 2 + .../src/app/providers/routing/routing.tsx | 2 + .../src/modules/agent/ui/SSHAgentForm.tsx | 23 +- frontend/src/pages/add-agents.page.tsx | 6 +- frontend/src/pages/registration.page.tsx | 423 ++++++++++++++++++ 5 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/registration.page.tsx diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx index 14a83ed..01f4887 100644 --- a/frontend/src/app/providers/layout/navigation/navigation.tsx +++ b/frontend/src/app/providers/layout/navigation/navigation.tsx @@ -6,6 +6,7 @@ import { FaUser, FaUsers, FaRocket, + FaKey, } from "react-icons/fa"; import { useAuthStore } from "@/modules/auth/store/useAuthStore"; @@ -17,6 +18,7 @@ export const Navigation = () => { const navItems = [ { path: "/", label: "Главная", icon: FaHome }, { path: "/add-agents", label: "Деплой", icon: FaRocket }, + { path: "/registration", label: "Регистрация", icon: FaKey }, { path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true }, { path: "/themes", label: "Темы", icon: FaPalette }, ]; diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx index c9b4e6d..3b436c4 100644 --- a/frontend/src/app/providers/routing/routing.tsx +++ b/frontend/src/app/providers/routing/routing.tsx @@ -10,6 +10,7 @@ import { DefaultLayout } from "@/shared/layouts/DefaultLayout"; import { AddAgentsPage } from "@/pages/add-agents.page"; import { IDEPage } from "@/pages/ide.page"; import { AdminPage } from "@/pages/admin.page"; +import { RegistrationTokenPage } from "@/pages/registration.page"; export const mockGraphData: GraphData = { nodes: [ @@ -120,6 +121,7 @@ export const Routing = () => { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/modules/agent/ui/SSHAgentForm.tsx b/frontend/src/modules/agent/ui/SSHAgentForm.tsx index d41fe1c..032dce4 100644 --- a/frontend/src/modules/agent/ui/SSHAgentForm.tsx +++ b/frontend/src/modules/agent/ui/SSHAgentForm.tsx @@ -21,6 +21,7 @@ interface ExtraField { } export interface SSHAgentConfig { + agentLabel: string; user: string; ip: string; port: number; @@ -191,7 +192,27 @@ export const SSHAgentForm: React.FC = ({
- {/* User и IP */} + {/* Agent Label */} +
+ + handleChange("agentLabel", e.target.value)} + required + style={inputBaseStyle} + onFocus={handleFocus} + onBlur={handleBlur} + placeholder="production-server-1" + /> +
+ + {/* User, IP и Port */}
({ + agentLabel: "", user: "", ip: "", port: 22, @@ -53,7 +54,8 @@ export const AddAgentsPage: React.FC = () => { // Валидация const isValid = agents.every((agent) => { - if (!agent.user || !agent.ip || !agent.port) return false; + if (!agent.agentLabel || !agent.user || !agent.ip || !agent.port) + return false; if (agent.port < 1 || agent.port > 65535) return false; if (agent.authMethod === "key" && !agent.sshKey) return false; if (agent.authMethod === "password" && !agent.password) return false; @@ -73,7 +75,7 @@ export const AddAgentsPage: React.FC = () => { // Преобразуем данные из формы в формат API const deployData: DeployAgentsRequest = { servers: agents.map((agent) => ({ - agentLabel: `${agent.ip}-${agent.user}`, + agentLabel: agent.agentLabel, ip: agent.ip, user: agent.user, port: agent.port, diff --git a/frontend/src/pages/registration.page.tsx b/frontend/src/pages/registration.page.tsx new file mode 100644 index 0000000..ecf464a --- /dev/null +++ b/frontend/src/pages/registration.page.tsx @@ -0,0 +1,423 @@ +import React, { useState } from "react"; +import { agentApiService } from "@/modules/agent/api/agent.api.service"; +import { FiKey, FiPlus, FiTrash2, FiCopy, FiCheck, FiX } from "react-icons/fi"; + +interface RegistrationTokenForm { + label: string; +} + +interface RegistrationResult { + label: string; + token: string; +} + +export const RegistrationTokenPage: React.FC = () => { + const [tokens, setTokens] = useState([ + { label: "" }, + ]); + const [results, setResults] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [copiedIndex, setCopiedIndex] = useState(null); + + const handleTokenChange = (index: number, label: string) => { + const newTokens = [...tokens]; + newTokens[index] = { label }; + setTokens(newTokens); + }; + + const handleAddToken = () => { + setTokens([...tokens, { label: "" }]); + }; + + const handleRemoveToken = (index: number) => { + const newTokens = tokens.filter((_, i) => i !== index); + setTokens(newTokens); + }; + + const handleCopyToken = async (token: string, index: number) => { + await navigator.clipboard.writeText(token); + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null), 2000); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Валидация + const validTokens = tokens.filter((t) => t.label.trim()); + if (validTokens.length === 0) { + setError("Введите хотя бы одну метку для токена"); + return; + } + + setIsSubmitting(true); + setError(null); + setSuccessMessage(null); + setResults([]); + + try { + const createdTokens: RegistrationResult[] = []; + + for (const tokenData of validTokens) { + const response = await agentApiService.createRegistrationToken({ + label: tokenData.label, + }); + + // API возвращает объект с токеном + const token = response.token || Object.values(response)[0] as string; + + createdTokens.push({ + label: tokenData.label, + token, + }); + } + + setResults(createdTokens); + setSuccessMessage( + `Успешно создано ${createdTokens.length} токен(ов)` + ); + setTokens([{ label: "" }]); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Ошибка при создании токенов" + ); + } finally { + setIsSubmitting(false); + } + }; + + const inputStyle: React.CSSProperties = { + width: "100%", + padding: "10px 12px", + border: "1px solid var(--border)", + borderRadius: "8px", + backgroundColor: "var(--input-bg)", + color: "var(--text-primary)", + fontSize: "14px", + transition: "border-color 0.2s, box-shadow 0.2s", + }; + + const labelStyle: React.CSSProperties = { + display: "block", + marginBottom: "8px", + color: "var(--text-secondary)", + fontSize: "14px", + fontWeight: 500, + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Регистрация токенов для агентов +

+

+ Создайте токены для регистрации новых агентов +

+
+
+
+ +
+ {/* Token Forms */} +
+ {tokens.map((token, index) => ( +
+ {/* Header */} +
+
+
+ +
+

+ Токен #{index + 1} +

+
+ {tokens.length > 1 && ( + + )} +
+ + {/* Label Input */} +
+ + handleTokenChange(index, e.target.value)} + required + style={inputStyle} + onFocus={(e) => { + e.currentTarget.style.borderColor = "var(--border-focus)"; + e.currentTarget.style.boxShadow = `0 0 0 3px var(--border-focus)30`; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = "var(--border)"; + e.currentTarget.style.boxShadow = "none"; + }} + placeholder="agent-production-1" + /> +
+
+ ))} +
+ + {/* Add Token Button */} + + + {/* Messages */} + {successMessage && ( +
+
+ {successMessage} + +
+
+ )} + + {error && ( +
+
+ {error} + +
+
+ )} + + {/* Submit Button */} + + + {/* Results */} + {results.length > 0 && ( +
+

+ Созданные токены +

+
+ {results.map((result, index) => ( +
+
+ + {result.label} + + +
+ + {result.token} + +
+ ))} +
+
+ )} +
+
+
+ ); +};