diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json index abf04de57..26fbe2bd8 100644 --- a/i18n/locales/fr.json +++ b/i18n/locales/fr.json @@ -287,6 +287,8 @@ "Save": "Enregistrer", "More": "Plus", "SaveFailed": "Impossible d’enregistrer le script", + "DeleteConfirm": "Supprimer « {name} » ?", + "DeleteFailed": "Impossible de supprimer le script", "FillBatchCommand": "Insérer dans la commande par lot", "GroupShell": "Shell", "GroupPowerShell": "PowerShell", diff --git a/ui/composables/useApiRequest.ts b/ui/composables/useApiRequest.ts index 771c64bca..89bb48b9e 100644 --- a/ui/composables/useApiRequest.ts +++ b/ui/composables/useApiRequest.ts @@ -723,6 +723,14 @@ export function getAccountDetail(accountId: string, orgId?: string): Promise { + return apiRequest({ + method: "GET", + path: `/api/v1/accounts/personal-asset-credentials/${encodeURIComponent(credentialId)}/`, + orgId + }); +} + export async function getPersonalAssetCredentials( assetId: string, protocol: string, diff --git a/ui/composables/useSessionWindowConnect.ts b/ui/composables/useSessionWindowConnect.ts index a40935fef..c91bbd3fd 100644 --- a/ui/composables/useSessionWindowConnect.ts +++ b/ui/composables/useSessionWindowConnect.ts @@ -1,5 +1,10 @@ import type { AssetDetail, AssetItem, PermedAccount, PermOrgItem, RdpGraphics } from "~/types"; -import { getAccountDetail, getAssetDetailRequest, getConsoleAssetDetail } from "~/composables/useApiRequest"; +import { + getAccountDetail, + getAssetDetailRequest, + getConsoleAssetDetail, + getPersonalAssetCredential +} from "~/composables/useApiRequest"; import { desktopInvoke } from "~/shared/desktop/bridge"; import { useUserInfoStore } from "~/store/modules/userInfo"; import { transformAssetDetail } from "~/utils"; @@ -11,6 +16,7 @@ export interface SessionWindowConnectionInfo { account: string; manualUsername: string; manualPassword: string; + personalCredentialId?: string; dynamicPassword: string; rememberSecret: boolean; rememberSelection?: boolean; @@ -122,6 +128,9 @@ export function buildSessionPath(asset: AssetItem, connectionInfo?: SessionWindo query.set("accountMode", connectionInfo.accountMode); if (connectionInfo.accountId) query.set("accountId", connectionInfo.accountId); if (connectionInfo.connectMethod) query.set("method", connectionInfo.connectMethod); + if (connectionInfo.accountMode === "manual" && connectionInfo.personalCredentialId) { + query.set("personalCredentialId", connectionInfo.personalCredentialId); + } return `/session/${encodeURIComponent(asset.id)}?${query.toString()}`; } @@ -162,6 +171,7 @@ const sessionAccountModes = new Set( export function useSessionWindowConnect() { const route = useRoute(); + const { t } = useI18n(); const { activeTab, openSession, openSetupSession } = useWorkspaceTabs(); const { confirmConnection } = useAssetConnection(); const userInfoStore = useUserInfoStore(); @@ -214,6 +224,51 @@ export function useSessionWindowConnect() { asset.savedConnection = saved || undefined; assetName.value = asset.name || "JumpServer"; + // An explicit credential must never fall back to a remembered account. + if (route.query.personalCredentialId !== undefined) { + const credentialId = queryValue(route.query.personalCredentialId); + if (admin || !credentialId) throw new Error(t("ConnectError.PersonalCredentialNotFound")); + + const credential = await getPersonalAssetCredential(credentialId, orgId); + if (credential.asset.id !== assetId || !credential.is_active || !credential.has_secret) { + throw new Error(t("ConnectError.PersonalCredentialNotFound")); + } + const protocol = typeof credential.protocol === "string" ? credential.protocol : credential.protocol.value; + if ( + !asset.permedProtocols?.some( + (item) => item.name === protocol && (isDesktopRuntime() || item.public !== false) + ) + ) { + throw new Error(t("ConnectError.ProtocolUnavailable")); + } + if (!asset.permedAccounts?.some((account) => account.alias === "@INPUT")) { + throw new Error(t("ConnectError.ManualAccountDenied")); + } + const preferred = + preference?.protocol === protocol ? preference : saved?.protocol === protocol ? saved : undefined; + const connectMethod = queryValue(route.query.method) || preferred?.connectMethod || ""; + const pane = openSession(asset, { protocol, account: credential.username, connectMethod }); + await confirmConnection(asset, { + protocol, + account: "@INPUT", + accountMode: "manual", + manualUsername: credential.username, + manualPassword: "", + personalCredentialId: credential.id, + personalCredentialVersion: credential.version, + personalCredentialSecretType: + typeof credential.secret_type === "string" ? credential.secret_type : credential.secret_type.value, + savePersonalCredential: false, + dynamicPassword: "", + rememberSecret: false, + preserveStoredSelection: true, + connectMethod, + connectOptions: preferred?.connectOptions || {}, + tabId: pane.id + }); + return; + } + const reusableSavedConnection = !admin && hasReusableSavedConnection(asset); const connection = { ...(saved || {}), ...(preference || {}), ...(routeConnection || {}) }; const queryNeedsNoSecret = routeConnection && ["hosted", "anonymous"].includes(routeConnection.accountMode);