From ee38abbecbdf0d6dd8261d0cb8173890b69311c7 Mon Sep 17 00:00:00 2001 From: dv Date: Tue, 14 Apr 2026 13:58:13 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D1=83=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D0=B0=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/devices.py | 23 ++++- frontend/src/api/objects.ts | 12 ++- frontend/src/pages/DeviceDetail.tsx | 145 ++++++++++++++-------------- 3 files changed, 102 insertions(+), 78 deletions(-) diff --git a/backend/app/routers/devices.py b/backend/app/routers/devices.py index 217c84a..d58c13f 100644 --- a/backend/app/routers/devices.py +++ b/backend/app/routers/devices.py @@ -238,10 +238,27 @@ async def get_device_credentials( db: AsyncSession = Depends(get_db), _: User = Depends(get_current_user), ): - """Return decrypted SSH password override for the device (if set).""" + """Return all decrypted passwords for the device: SSH (device override + object level) and connection_params.""" device = await _get_device(db, obj_id, device_id) - password = crypto.decrypt(device.ssh_pass_override_enc) if device.ssh_pass_override_enc else None - return {"ssh_password": password} + obj = await _get_object(db, obj_id) + + ssh_password_device = crypto.decrypt(device.ssh_pass_override_enc) if device.ssh_pass_override_enc else None + ssh_password_object = crypto.decrypt(obj.ssh_pass_enc) if obj.ssh_pass_enc else None + + # connection_params.password may be a Fernet ciphertext — try to decrypt, fall back to raw + connection_password: str | None = None + raw_conn_pass = (device.connection_params or {}).get("password") + if raw_conn_pass: + try: + connection_password = crypto.decrypt(str(raw_conn_pass)) + except Exception: + connection_password = str(raw_conn_pass) + + return { + "ssh_password_device": ssh_password_device, + "ssh_password_object": ssh_password_object, + "connection_password": connection_password, + } @router.patch("/{device_id}", response_model=DeviceRead) diff --git a/frontend/src/api/objects.ts b/frontend/src/api/objects.ts index 8ef9ae3..1c159a8 100644 --- a/frontend/src/api/objects.ts +++ b/frontend/src/api/objects.ts @@ -351,14 +351,20 @@ export const useImportObjectsXLSX = () => { }) } -export const useDeviceCredentials = (objId: number, deviceId: number, enabled: boolean) => +export interface DeviceCredentials { + ssh_password_device: string | null + ssh_password_object: string | null + connection_password: string | null +} + +export const useDeviceCredentials = (objId: number, deviceId: number) => useQuery({ queryKey: ['device-credentials', objId, deviceId], queryFn: () => api - .get<{ ssh_password: string | null }>(`/api/v1/objects/${objId}/devices/${deviceId}/credentials`) + .get(`/api/v1/objects/${objId}/devices/${deviceId}/credentials`) .then((r) => r.data), - enabled: enabled && !!objId && !!deviceId, + enabled: !!objId && !!deviceId, staleTime: 60_000, }) diff --git a/frontend/src/pages/DeviceDetail.tsx b/frontend/src/pages/DeviceDetail.tsx index b782b9b..efa9a14 100644 --- a/frontend/src/pages/DeviceDetail.tsx +++ b/frontend/src/pages/DeviceDetail.tsx @@ -4,7 +4,6 @@ import { DeleteOutlined, EditOutlined, EyeOutlined, - EyeInvisibleOutlined, LinkOutlined, ThunderboltOutlined, FileSearchOutlined, @@ -38,7 +37,7 @@ import dayjs from 'dayjs' import { useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useAlerts, useAcknowledgeAlert, useResolveAlert, type AlertItem } from '../api/alerts' -import { useDevice, useDeleteDevice, useObject, useDeviceCredentials, type DeviceItem, type ObjectItem } from '../api/objects' +import { useDevice, useDeleteDevice, useObject, useDeviceCredentials, type DeviceItem, type ObjectItem, type DeviceCredentials } from '../api/objects' import { getDeviceWebUrl } from '../utils/deviceUrl' import { elapsedAgo } from '../utils/time' import { useSnapshots, useSnapshotDiff } from '../api/snapshots' @@ -101,13 +100,19 @@ function getSnapshotConfig(device: DeviceItem) { // ─── SSH / Credentials block ────────────────────────────────────────────────── -function SshBlock({ device, objId, obj }: { device: DeviceItem; objId: number; obj: ObjectItem | undefined }) { - const [showPassword, setShowPassword] = useState(false) - const { data: creds, isFetching: credsFetching } = useDeviceCredentials(objId, device.id, showPassword) - +function SshBlock({ + device, + obj, + creds, + credsLoading, +}: { + device: DeviceItem + obj: ObjectItem | undefined + creds: DeviceCredentials | undefined + credsLoading: boolean +}) { const lastAuth = device.ssh_last_auth - // Resolve effective values: device override → object default → fallback const effectiveUser = lastAuth?.username ? String(lastAuth.username) @@ -115,29 +120,29 @@ function SshBlock({ device, objId, obj }: { device: DeviceItem; objId: number; o const effectivePort = device.ssh_port_override ?? 22 - // Determine auth method from last_auth - const authType = lastAuth?.type ? String(lastAuth.type) : null - const usesKey = authType === 'key' || device.ssh_key_id_override != null - const credSource: 'device' | 'object' | 'unknown' = - lastAuth?.cred_id != null ? 'object' - : lastAuth?.key_id != null || device.ssh_key_id_override != null ? 'device' - : device.ssh_user_override ? 'device' - : 'object' + const usesKey = String(lastAuth?.type) === 'key' || device.ssh_key_id_override != null + + // Effective SSH password: device override first, then object level + const effectiveSshPassword = creds?.ssh_password_device ?? creds?.ssh_password_object ?? null + const sshPasswordSource = creds?.ssh_password_device + ? 'device' + : creds?.ssh_password_object + ? 'объект' + : null return ( SSH / Авторизация} > - {/* Effective login */} {effectiveUser - ? {effectiveUser} + ? {effectiveUser} : Не задан} {device.ssh_user_override ? device @@ -147,8 +152,7 @@ function SshBlock({ device, objId, obj }: { device: DeviceItem; objId: number; o - {/* Effective auth method + password */} - + {usesKey ? ( SSH ключ @@ -158,39 +162,18 @@ function SshBlock({ device, objId, obj }: { device: DeviceItem; objId: number; o )} - ) : ( - - Пароль - {credSource === 'object' ? ( - - с объекта{lastAuth?.cred_id != null ? ` (cred_id=${String(lastAuth.cred_id)})` : ''} - - ) : ( - <> - {showPassword ? ( - credsFetching - ? - : creds?.ssh_password - ? {creds.ssh_password} - : Не задан - ) : ( - •••••••• - )} - - - )} + ) : credsLoading ? ( + + ) : effectiveSshPassword ? ( + + {effectiveSshPassword} + {sshPasswordSource && {sshPasswordSource}} + ) : ( + Не задан )} - {/* Port */} {effectivePort} @@ -198,14 +181,15 @@ function SshBlock({ device, objId, obj }: { device: DeviceItem; objId: number; o - {/* Last successful connection */} {lastAuth && ( - + успешно - {authType && {authType === 'key' ? 'ключ' : 'пароль'}} + {String(lastAuth.type) === 'key' ? 'ключ' : 'пароль'} {lastAuth.username && ( - {String(lastAuth.username)} + + {String(lastAuth.username)} + )} @@ -292,7 +276,15 @@ function DeviceMetaBlock({ device }: { device: DeviceItem }) { // ─── Connection params smart render ────────────────────────────────────────── -function ConnectionParamsBlock({ device }: { device: DeviceItem }) { +function ConnectionParamsBlock({ + device, + creds, + credsLoading, +}: { + device: DeviceItem + creds: DeviceCredentials | undefined + credsLoading: boolean +}) { const params = device.connection_params if (!params || Object.keys(params).length === 0) return null @@ -301,6 +293,7 @@ function ConnectionParamsBlock({ device }: { device: DeviceItem }) { const remaining: Record = {} for (const [k, v] of Object.entries(params)) { + if (k === 'password') continue // shown separately, decrypted if (knownKeys.includes(k)) displayed[k] = v else remaining[k] = v } @@ -314,7 +307,6 @@ function ConnectionParamsBlock({ device }: { device: DeviceItem }) { api_port: 'API порт', web_port: 'Web порт', } - const AUTH_COLOR: Record = { basic: 'blue', digest: 'orange', @@ -324,24 +316,32 @@ function ConnectionParamsBlock({ device }: { device: DeviceItem }) { const hasDisplayed = Object.keys(displayed).length > 0 const hasRemaining = Object.keys(remaining).length > 0 + const hasPassword = 'password' in params return ( - {hasDisplayed && ( - - {Object.entries(displayed).map(([k, v]) => ( - - {k === 'auth_type' - ? {String(v)} - : k === 'protocol' - ? {String(v).toUpperCase()} - : k === 'url' - ? {String(v)} - : {String(v)}} - - ))} - - )} + + {hasDisplayed && Object.entries(displayed).map(([k, v]) => ( + + {k === 'auth_type' + ? {String(v)} + : k === 'protocol' + ? {String(v).toUpperCase()} + : k === 'url' + ? {String(v)} + : {String(v)}} + + ))} + {hasPassword && ( + + {credsLoading + ? + : creds?.connection_password + ? {creds.connection_password} + : Не задан} + + )} + {hasRemaining && (
           {JSON.stringify(remaining, null, 2)}
@@ -887,6 +887,7 @@ export function DeviceDetailPage() {
   const { data: obj } = useObject(objId)
   const { data: device, isLoading } = useDevice(objId, deviceId)
   const { data: tasks } = useDeviceTasks(deviceId)
+  const { data: creds, isLoading: credsLoading } = useDeviceCredentials(objId, deviceId)
   const deleteDevice = useDeleteDevice(objId)
   const createTask = useCreateTask()
 
@@ -1054,7 +1055,7 @@ export function DeviceDetailPage() {
             
 
             {/* SSH block — always shown */}
-            
+            
 
             {/* System info (Linux-like devices) */}
             {isLinuxLike && }
@@ -1088,7 +1089,7 @@ export function DeviceDetailPage() {
               
             )}
             
-            
+            
           
         
       ),