fix: gate agent commands until token step confirmed
This commit is contained in:
parent
f800d7d59d
commit
45e6ff48eb
2 changed files with 160 additions and 10 deletions
|
|
@ -45,6 +45,7 @@ export const DockerAgents: Component = () => {
|
|||
const [newTokenName, setNewTokenName] = createSignal('');
|
||||
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
||||
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
||||
const [stepTwoComplete, setStepTwoComplete] = createSignal(false);
|
||||
|
||||
const tokenStepLabel = 'Step 2 · Generate API token';
|
||||
const commandStepLabel = 'Step 3 · Install command';
|
||||
|
|
@ -59,6 +60,12 @@ export const DockerAgents: Component = () => {
|
|||
legacyKeys: ['apiToken'],
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!apiToken()) {
|
||||
setStepTwoComplete(false);
|
||||
}
|
||||
});
|
||||
|
||||
const pulseUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const protocol = window.location.protocol;
|
||||
|
|
@ -167,7 +174,21 @@ const modalCommandProgress = createMemo(() => {
|
|||
fetchSecurityStatus();
|
||||
});
|
||||
|
||||
const commandReady = () => !requiresToken() || Boolean(apiToken());
|
||||
const tokenReady = () => !requiresToken() || Boolean(apiToken());
|
||||
const commandsUnlocked = () => tokenReady() && stepTwoComplete();
|
||||
|
||||
const acknowledgeTokenUse = () => {
|
||||
if (!requiresToken()) {
|
||||
setStepTwoComplete(true);
|
||||
return;
|
||||
}
|
||||
if (apiToken()) {
|
||||
setStepTwoComplete(true);
|
||||
notificationStore.success('Token ready to use in the install command.', 3500);
|
||||
} else {
|
||||
notificationStore.info('Generate or select a token before continuing.', 4000);
|
||||
}
|
||||
};
|
||||
|
||||
// Find the token record that matches the currently stored token
|
||||
const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||
|
|
@ -206,6 +227,7 @@ const modalCommandProgress = createMemo(() => {
|
|||
const defaultName = `Docker host ${new Date().toISOString().slice(0, 10)}`;
|
||||
setNewTokenName(defaultName);
|
||||
setShowGenerateTokenModal(true);
|
||||
setStepTwoComplete(false);
|
||||
};
|
||||
|
||||
const handleCreateToken = async () => {
|
||||
|
|
@ -218,6 +240,7 @@ const modalCommandProgress = createMemo(() => {
|
|||
setShowGenerateTokenModal(false);
|
||||
setNewTokenName('');
|
||||
setLatestRecord(record);
|
||||
setStepTwoComplete(true);
|
||||
showTokenReveal({
|
||||
token,
|
||||
record,
|
||||
|
|
@ -472,10 +495,56 @@ WantedBy=multi-user.target`;
|
|||
>
|
||||
{isGeneratingToken() ? 'Generating…' : 'Generate token'}
|
||||
</button>
|
||||
|
||||
<Show when={apiToken()}>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<div class={`flex-1 rounded-lg border px-4 py-2 text-xs ${
|
||||
stepTwoComplete()
|
||||
? 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200'
|
||||
: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200'
|
||||
}`}>
|
||||
{stepTwoComplete()
|
||||
? 'Token ready. You can continue to the install command.'
|
||||
: 'Stored token detected. Confirm to insert it into the install command.'}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledgeTokenUse}
|
||||
disabled={stepTwoComplete()}
|
||||
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
||||
stepTwoComplete()
|
||||
? 'bg-green-600 text-white cursor-default'
|
||||
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{stepTwoComplete() ? 'Token ready' : 'Use token in commands'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={commandReady()}>
|
||||
<Show when={!requiresToken()}>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-700 dark:bg-amber-900/30 dark:text-amber-200">
|
||||
Tokens are optional on this Pulse instance. Confirm to continue to the install command.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledgeTokenUse}
|
||||
disabled={stepTwoComplete()}
|
||||
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
||||
stepTwoComplete()
|
||||
? 'bg-green-600 text-white cursor-default'
|
||||
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{stepTwoComplete() ? 'Confirmed' : 'Confirm and continue'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={commandsUnlocked()}>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{commandStepLabel}</label>
|
||||
|
|
@ -488,10 +557,9 @@ WantedBy=multi-user.target`;
|
|||
window.showToast(success ? 'success' : 'error', success ? 'Copied!' : 'Failed to copy');
|
||||
}
|
||||
}}
|
||||
disabled={requiresToken() && !apiToken()}
|
||||
class="px-3 py-1.5 text-xs font-medium rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-blue-600 text-white hover:bg-blue-700"
|
||||
class="px-3 py-1.5 text-xs font-medium rounded transition-colors bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Copy
|
||||
Copy first command
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative rounded-lg border-2 border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 p-3 overflow-x-auto">
|
||||
|
|
@ -505,6 +573,15 @@ WantedBy=multi-user.target`;
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={requiresToken() && (!apiToken() || !stepTwoComplete())}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Generate a new token or confirm the stored one to unlock the install command.
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={!requiresToken() && !stepTwoComplete()}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Confirm the no-token setup to continue.</p>
|
||||
</Show>
|
||||
|
||||
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
|
||||
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Advanced options (uninstall & manual install)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import { Component, For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { useWebSocket } from '@/App';
|
||||
import type { Host } from '@/types/api';
|
||||
|
|
@ -168,6 +168,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
const [newTokenName, setNewTokenName] = createSignal('');
|
||||
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
||||
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
||||
const [stepTwoComplete, setStepTwoComplete] = createSignal(false);
|
||||
|
||||
const {
|
||||
token: apiToken,
|
||||
|
|
@ -180,6 +181,12 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
legacyKeys: ['apiToken'],
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!apiToken()) {
|
||||
setStepTwoComplete(false);
|
||||
}
|
||||
});
|
||||
|
||||
const hosts = createMemo(() => {
|
||||
const list = state.hosts ?? [];
|
||||
const filters = platformFilters[variant];
|
||||
|
|
@ -234,13 +241,28 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const commandReady = () => !requiresToken() || Boolean(apiToken());
|
||||
const tokenReady = () => !requiresToken() || Boolean(apiToken());
|
||||
const commandsUnlocked = () => tokenReady() && stepTwoComplete();
|
||||
|
||||
const acknowledgeTokenUse = () => {
|
||||
if (!requiresToken()) {
|
||||
setStepTwoComplete(true);
|
||||
return;
|
||||
}
|
||||
if (apiToken()) {
|
||||
setStepTwoComplete(true);
|
||||
notificationStore.success('Token ready to embed in the install commands.', 3500);
|
||||
} else {
|
||||
notificationStore.info('Generate or select a token before continuing.', 4000);
|
||||
}
|
||||
};
|
||||
|
||||
const openGenerateTokenModal = () => {
|
||||
setGenerateError(null);
|
||||
const defaultName = `Host agent ${new Date().toISOString().slice(0, 10)}`;
|
||||
setNewTokenName(defaultName);
|
||||
setShowGenerateTokenModal(true);
|
||||
setStepTwoComplete(false);
|
||||
};
|
||||
|
||||
const handleCreateToken = async () => {
|
||||
|
|
@ -254,6 +276,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
setShowGenerateTokenModal(false);
|
||||
setNewTokenName('');
|
||||
setLatestRecord(record);
|
||||
setStepTwoComplete(true);
|
||||
showTokenReveal({
|
||||
token,
|
||||
record,
|
||||
|
|
@ -382,6 +405,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
setGenerateError(null);
|
||||
setLatestRecord(null);
|
||||
setApiToken(null);
|
||||
setStepTwoComplete(false);
|
||||
}}
|
||||
>
|
||||
<p class="font-semibold text-gray-900 dark:text-gray-100">{option.label}</p>
|
||||
|
|
@ -431,10 +455,56 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
>
|
||||
{isGeneratingToken() ? 'Generating…' : 'Generate token'}
|
||||
</button>
|
||||
|
||||
<Show when={apiToken()}>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<div class={`flex-1 rounded-lg border px-4 py-2 text-xs ${
|
||||
stepTwoComplete()
|
||||
? 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200'
|
||||
: 'border-blue-200 bg-blue-50 text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200'
|
||||
}`}>
|
||||
{stepTwoComplete()
|
||||
? 'Token ready. You can continue to the commands.'
|
||||
: 'Stored token detected. Confirm to insert it into the commands below.'}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledgeTokenUse}
|
||||
disabled={stepTwoComplete()}
|
||||
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
||||
stepTwoComplete()
|
||||
? 'bg-green-600 text-white cursor-default'
|
||||
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{stepTwoComplete() ? 'Token ready' : 'Use token in commands'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={commandReady()}>
|
||||
<Show when={!requiresToken()}>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-700 dark:bg-amber-900/30 dark:text-amber-200">
|
||||
Tokens are optional on this Pulse instance. Confirm to continue to the install commands.
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={acknowledgeTokenUse}
|
||||
disabled={stepTwoComplete()}
|
||||
class={`inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
||||
stepTwoComplete()
|
||||
? 'bg-green-600 text-white cursor-default'
|
||||
: 'bg-gray-900 text-white hover:bg-black dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white'
|
||||
}`}
|
||||
>
|
||||
{stepTwoComplete() ? 'Confirmed' : 'Confirm and continue'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={commandsUnlocked()}>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">{commandStepLabel()}</h4>
|
||||
|
|
@ -487,11 +557,14 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={requiresToken() && !commandReady()}>
|
||||
<Show when={requiresToken() && (!apiToken() || !stepTwoComplete())}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Select or generate an API token to embed it in the install commands.
|
||||
Generate a new token or confirm the stored one to unlock the install commands.
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={!requiresToken() && !stepTwoComplete()}>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Confirm the no-token setup to continue.</p>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Reference in a new issue