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 [newTokenName, setNewTokenName] = createSignal('');
|
||||||
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
||||||
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
||||||
|
const [stepTwoComplete, setStepTwoComplete] = createSignal(false);
|
||||||
|
|
||||||
const tokenStepLabel = 'Step 2 · Generate API token';
|
const tokenStepLabel = 'Step 2 · Generate API token';
|
||||||
const commandStepLabel = 'Step 3 · Install command';
|
const commandStepLabel = 'Step 3 · Install command';
|
||||||
|
|
@ -59,6 +60,12 @@ export const DockerAgents: Component = () => {
|
||||||
legacyKeys: ['apiToken'],
|
legacyKeys: ['apiToken'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!apiToken()) {
|
||||||
|
setStepTwoComplete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const pulseUrl = () => {
|
const pulseUrl = () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
|
|
@ -167,7 +174,21 @@ const modalCommandProgress = createMemo(() => {
|
||||||
fetchSecurityStatus();
|
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
|
// Find the token record that matches the currently stored token
|
||||||
const copyToClipboard = async (text: string): Promise<boolean> => {
|
const copyToClipboard = async (text: string): Promise<boolean> => {
|
||||||
|
|
@ -206,6 +227,7 @@ const modalCommandProgress = createMemo(() => {
|
||||||
const defaultName = `Docker host ${new Date().toISOString().slice(0, 10)}`;
|
const defaultName = `Docker host ${new Date().toISOString().slice(0, 10)}`;
|
||||||
setNewTokenName(defaultName);
|
setNewTokenName(defaultName);
|
||||||
setShowGenerateTokenModal(true);
|
setShowGenerateTokenModal(true);
|
||||||
|
setStepTwoComplete(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateToken = async () => {
|
const handleCreateToken = async () => {
|
||||||
|
|
@ -218,6 +240,7 @@ const modalCommandProgress = createMemo(() => {
|
||||||
setShowGenerateTokenModal(false);
|
setShowGenerateTokenModal(false);
|
||||||
setNewTokenName('');
|
setNewTokenName('');
|
||||||
setLatestRecord(record);
|
setLatestRecord(record);
|
||||||
|
setStepTwoComplete(true);
|
||||||
showTokenReveal({
|
showTokenReveal({
|
||||||
token,
|
token,
|
||||||
record,
|
record,
|
||||||
|
|
@ -472,10 +495,56 @@ WantedBy=multi-user.target`;
|
||||||
>
|
>
|
||||||
{isGeneratingToken() ? 'Generating…' : 'Generate token'}
|
{isGeneratingToken() ? 'Generating…' : 'Generate token'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</Show>
|
</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="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">{commandStepLabel}</label>
|
<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');
|
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 bg-blue-600 text-white hover:bg-blue-700"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
Copy
|
Copy first command
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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>
|
</div>
|
||||||
</Show>
|
</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">
|
<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">
|
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
Advanced options (uninstall & manual install)
|
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 type { JSX } from 'solid-js';
|
||||||
import { useWebSocket } from '@/App';
|
import { useWebSocket } from '@/App';
|
||||||
import type { Host } from '@/types/api';
|
import type { Host } from '@/types/api';
|
||||||
|
|
@ -168,6 +168,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
||||||
const [newTokenName, setNewTokenName] = createSignal('');
|
const [newTokenName, setNewTokenName] = createSignal('');
|
||||||
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
const [generateError, setGenerateError] = createSignal<string | null>(null);
|
||||||
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
const [latestRecord, setLatestRecord] = createSignal<APITokenRecord | null>(null);
|
||||||
|
const [stepTwoComplete, setStepTwoComplete] = createSignal(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
token: apiToken,
|
token: apiToken,
|
||||||
|
|
@ -180,6 +181,12 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
||||||
legacyKeys: ['apiToken'],
|
legacyKeys: ['apiToken'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!apiToken()) {
|
||||||
|
setStepTwoComplete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const hosts = createMemo(() => {
|
const hosts = createMemo(() => {
|
||||||
const list = state.hosts ?? [];
|
const list = state.hosts ?? [];
|
||||||
const filters = platformFilters[variant];
|
const filters = platformFilters[variant];
|
||||||
|
|
@ -234,13 +241,28 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
||||||
return true;
|
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 = () => {
|
const openGenerateTokenModal = () => {
|
||||||
setGenerateError(null);
|
setGenerateError(null);
|
||||||
const defaultName = `Host agent ${new Date().toISOString().slice(0, 10)}`;
|
const defaultName = `Host agent ${new Date().toISOString().slice(0, 10)}`;
|
||||||
setNewTokenName(defaultName);
|
setNewTokenName(defaultName);
|
||||||
setShowGenerateTokenModal(true);
|
setShowGenerateTokenModal(true);
|
||||||
|
setStepTwoComplete(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateToken = async () => {
|
const handleCreateToken = async () => {
|
||||||
|
|
@ -254,6 +276,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
||||||
setShowGenerateTokenModal(false);
|
setShowGenerateTokenModal(false);
|
||||||
setNewTokenName('');
|
setNewTokenName('');
|
||||||
setLatestRecord(record);
|
setLatestRecord(record);
|
||||||
|
setStepTwoComplete(true);
|
||||||
showTokenReveal({
|
showTokenReveal({
|
||||||
token,
|
token,
|
||||||
record,
|
record,
|
||||||
|
|
@ -382,6 +405,7 @@ export const HostAgents: Component<HostAgentsProps> = (props) => {
|
||||||
setGenerateError(null);
|
setGenerateError(null);
|
||||||
setLatestRecord(null);
|
setLatestRecord(null);
|
||||||
setApiToken(null);
|
setApiToken(null);
|
||||||
|
setStepTwoComplete(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p class="font-semibold text-gray-900 dark:text-gray-100">{option.label}</p>
|
<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'}
|
{isGeneratingToken() ? 'Generating…' : 'Generate token'}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</Show>
|
</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="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">{commandStepLabel()}</h4>
|
<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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={requiresToken() && !commandReady()}>
|
<Show when={requiresToken() && (!apiToken() || !stepTwoComplete())}>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</Show>
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue