- New SetupWizard component with 5-step flow: 1. Welcome: Bootstrap token unlock, platform showcase 2. Security: Admin account + API token creation 3. Connect: Multi-platform infrastructure (Proxmox, Docker, K8s) 4. Features: AI and auto-updates toggles 5. Complete: Credentials display with copy/download - Replaced FirstRunSetup with SetupWizard in Login.tsx - Added Install Update button to UpdatesSettingsPanel - Enhanced UpdatesSettingsPanel with update plan integration - Added UpdateConfirmationModal to Settings for inline updates Positions Pulse as a unified infrastructure monitoring platform, not just a Proxmox-specific tool.
191 lines
8 KiB
TypeScript
191 lines
8 KiB
TypeScript
import { Component, createSignal, Show } from 'solid-js';
|
||
import { showError, showSuccess } from '@/utils/toast';
|
||
import { setApiToken as setApiClientToken } from '@/utils/apiClient';
|
||
import type { WizardState } from '../SetupWizard';
|
||
|
||
interface SecurityStepProps {
|
||
state: WizardState;
|
||
updateState: (updates: Partial<WizardState>) => void;
|
||
bootstrapToken: string;
|
||
onNext: () => void;
|
||
onBack: () => void;
|
||
}
|
||
|
||
export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||
const [username, setUsername] = createSignal(props.state.username || 'admin');
|
||
const [useCustomPassword, setUseCustomPassword] = createSignal(false);
|
||
const [password, setPassword] = createSignal('');
|
||
const [confirmPassword, setConfirmPassword] = createSignal('');
|
||
const [isSettingUp, setIsSettingUp] = createSignal(false);
|
||
|
||
const generatePassword = () => {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
|
||
let pass = '';
|
||
for (let i = 0; i < 16; i++) {
|
||
pass += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return pass;
|
||
};
|
||
|
||
const generateToken = (): string => {
|
||
const array = new Uint8Array(24);
|
||
crypto.getRandomValues(array);
|
||
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
||
};
|
||
|
||
const handleSetup = async () => {
|
||
if (useCustomPassword()) {
|
||
if (!password() || password().length < 12) {
|
||
showError('Password must be at least 12 characters');
|
||
return;
|
||
}
|
||
if (password() !== confirmPassword()) {
|
||
showError('Passwords do not match');
|
||
return;
|
||
}
|
||
}
|
||
|
||
setIsSettingUp(true);
|
||
const finalPassword = useCustomPassword() ? password() : generatePassword();
|
||
const token = generateToken();
|
||
|
||
try {
|
||
setApiClientToken(token);
|
||
|
||
const response = await fetch('/api/security/quick-setup', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Setup-Token': props.bootstrapToken,
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
username: username(),
|
||
password: finalPassword,
|
||
apiToken: token,
|
||
force: false,
|
||
setupToken: props.bootstrapToken,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(await response.text());
|
||
}
|
||
|
||
props.updateState({
|
||
username: username(),
|
||
password: finalPassword,
|
||
apiToken: token,
|
||
});
|
||
|
||
showSuccess('Security configured!');
|
||
props.onNext();
|
||
} catch (error) {
|
||
showError(`Setup failed: ${error}`);
|
||
} finally {
|
||
setIsSettingUp(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div class="bg-white/10 backdrop-blur-xl rounded-2xl border border-white/20 overflow-hidden">
|
||
<div class="p-6 border-b border-white/10">
|
||
<h2 class="text-2xl font-bold text-white">Secure Your Dashboard</h2>
|
||
<p class="text-white/70 mt-1">Create your admin account</p>
|
||
</div>
|
||
|
||
<div class="p-6 space-y-6">
|
||
{/* Username */}
|
||
<div>
|
||
<label class="block text-sm font-medium text-white/80 mb-2">Username</label>
|
||
<input
|
||
type="text"
|
||
value={username()}
|
||
onInput={(e) => setUsername(e.currentTarget.value)}
|
||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="admin"
|
||
/>
|
||
</div>
|
||
|
||
{/* Password choice */}
|
||
<div>
|
||
<label class="block text-sm font-medium text-white/80 mb-3">Password</label>
|
||
<div class="grid grid-cols-2 gap-3 mb-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => setUseCustomPassword(false)}
|
||
class={`py-3 px-4 rounded-xl text-sm font-medium transition-all ${!useCustomPassword()
|
||
? 'bg-blue-500 text-white'
|
||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||
}`}
|
||
>
|
||
🔐 Generate Secure
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setUseCustomPassword(true)}
|
||
class={`py-3 px-4 rounded-xl text-sm font-medium transition-all ${useCustomPassword()
|
||
? 'bg-blue-500 text-white'
|
||
: 'bg-white/10 text-white/70 hover:bg-white/20'
|
||
}`}
|
||
>
|
||
✏️ Custom Password
|
||
</button>
|
||
</div>
|
||
|
||
<Show when={useCustomPassword()}>
|
||
<div class="space-y-3">
|
||
<input
|
||
type="password"
|
||
value={password()}
|
||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Password (min 12 characters)"
|
||
/>
|
||
<input
|
||
type="password"
|
||
value={confirmPassword()}
|
||
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
|
||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
placeholder="Confirm password"
|
||
/>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={!useCustomPassword()}>
|
||
<div class="bg-blue-500/20 border border-blue-400/30 rounded-xl p-4">
|
||
<p class="text-sm text-blue-200">
|
||
A secure 16-character password will be generated and shown after setup.
|
||
</p>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
{/* Info */}
|
||
<div class="bg-white/5 rounded-xl p-4">
|
||
<p class="text-sm text-white/60">
|
||
This creates your admin account and an API token for automation.
|
||
Credentials will be displayed once - save them securely!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div class="p-6 bg-black/20 flex gap-3">
|
||
<button
|
||
onClick={props.onBack}
|
||
class="px-6 py-3 bg-white/10 hover:bg-white/20 text-white rounded-xl transition-all"
|
||
>
|
||
← Back
|
||
</button>
|
||
<button
|
||
onClick={handleSetup}
|
||
disabled={isSettingUp()}
|
||
class="flex-1 py-3 px-6 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 disabled:opacity-50 text-white font-medium rounded-xl transition-all"
|
||
>
|
||
{isSettingUp() ? 'Setting up...' : 'Create Account →'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|