Install.vue et Settings.vue : remplace le champ unique "PVEAPIToken=..." par deux inputs distincts — Token ID (ex: enzo@pam!panel) et Secret (uuid). L'assemblage PVEAPIToken=ID=Secret se fait côté frontend avant envoi. Plus besoin de connaître le format interne. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
487 lines
14 KiB
Vue
487 lines
14 KiB
Vue
<template>
|
|
<!-- Page d'installation — wizard multi-étapes -->
|
|
<div class="install-page">
|
|
<div class="install-container">
|
|
<!-- En-tête -->
|
|
<div class="install-header">
|
|
<div class="install-logo">PX</div>
|
|
<h1>ProxmoxPanel</h1>
|
|
<p class="install-subtitle">{{ t('install.subtitle') }}</p>
|
|
</div>
|
|
|
|
<!-- Indicateur de progression -->
|
|
<div class="install-steps">
|
|
<div
|
|
v-for="(step, i) in steps"
|
|
:key="i"
|
|
class="install-step"
|
|
:class="{
|
|
'install-step--active': currentStep === i,
|
|
'install-step--done': currentStep > i,
|
|
}"
|
|
>
|
|
<div class="install-step__dot">
|
|
<svg v-if="currentStep > i" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
<span v-else>{{ i + 1 }}</span>
|
|
</div>
|
|
<span class="install-step__label">{{ t(step.label) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contenu des étapes -->
|
|
<div class="neu-card install-card">
|
|
<!-- Étape 1 : Configuration générale -->
|
|
<div v-if="currentStep === 0">
|
|
<h2>{{ t('install.step1.title') }}</h2>
|
|
<p class="step-desc">{{ t('install.step1.desc') }}</p>
|
|
|
|
<div class="form-group">
|
|
<label>{{ t('install.instanceName') }}</label>
|
|
<input v-model="form.instanceName" class="neu-input" :placeholder="t('install.instanceNamePlaceholder')" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.publicUrl') }}</label>
|
|
<input v-model="form.publicUrl" class="neu-input" :placeholder="detectedURL" />
|
|
<small>{{ t('install.publicUrlHint', { url: detectedURL }) }}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.defaultLang') }}</label>
|
|
<select v-model="form.defaultLang" class="neu-input">
|
|
<option value="fr">Français</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Étape 2 : Configuration SSH -->
|
|
<div v-if="currentStep === 1">
|
|
<h2>{{ t('install.step2.title') }}</h2>
|
|
<p class="step-desc">{{ t('install.step2.desc') }}</p>
|
|
|
|
<div class="form-group">
|
|
<label>{{ t('install.sshHost') }}</label>
|
|
<input v-model="form.sshHost" class="neu-input" placeholder="10.0.0.1:2244" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.sshUsername') }}</label>
|
|
<input v-model="form.sshUsername" class="neu-input" placeholder="enzo" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.sshPassword') }}</label>
|
|
<input v-model="form.sshPassword" type="password" class="neu-input" />
|
|
</div>
|
|
|
|
<!-- Bouton test SSH -->
|
|
<button
|
|
class="neu-btn neu-btn--primary"
|
|
:disabled="testingSSH || !form.sshHost || !form.sshUsername || !form.sshPassword"
|
|
@click="testSSH"
|
|
>
|
|
<span v-if="testingSSH" class="neu-loading">⟳</span>
|
|
{{ t('install.testSSH') }}
|
|
</button>
|
|
|
|
<div v-if="sshTestResult" :class="['install-result', sshTestResult.success ? 'install-result--success' : 'install-result--error']">
|
|
{{ sshTestResult.message }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Étape 3 : Token Proxmox -->
|
|
<div v-if="currentStep === 2">
|
|
<h2>{{ t('install.step3.title') }}</h2>
|
|
<p class="step-desc">{{ t('install.step3.desc') }}</p>
|
|
|
|
<div class="form-group">
|
|
<label>{{ t('install.proxmoxUrl') }}</label>
|
|
<input v-model="form.proxmoxUrl" class="neu-input" placeholder="https://10.0.0.1:8006" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.proxmoxTokenId') }}</label>
|
|
<input v-model="form.proxmoxTokenId" class="neu-input" placeholder="enzo@pam!panel" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>{{ t('install.proxmoxTokenSecret') }}</label>
|
|
<input v-model="form.proxmoxTokenSecret" type="password" class="neu-input" placeholder="ed57ea62-cadc-4ddd-..." />
|
|
<small>{{ t('install.proxmoxTokenHint') }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Étape 4 : Confirmation -->
|
|
<div v-if="currentStep === 3">
|
|
<h2>{{ t('install.step4.title') }}</h2>
|
|
<div class="install-summary">
|
|
<div class="summary-item">
|
|
<span class="summary-key">{{ t('install.instanceName') }}</span>
|
|
<span class="summary-value">{{ form.instanceName }}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-key">{{ t('install.sshHost') }}</span>
|
|
<span class="summary-value">{{ form.sshHost }}</span>
|
|
</div>
|
|
<div class="summary-item">
|
|
<span class="summary-key">{{ t('install.defaultLang') }}</span>
|
|
<span class="summary-value">{{ form.defaultLang === 'fr' ? 'Français' : 'English' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Erreur globale -->
|
|
<div v-if="error" class="install-result install-result--error">{{ error }}</div>
|
|
|
|
<!-- Actions navigation -->
|
|
<div class="install-actions">
|
|
<button v-if="currentStep > 0" class="neu-btn" @click="currentStep--">
|
|
{{ t('install.back') }}
|
|
</button>
|
|
<div class="spacer" />
|
|
<button
|
|
v-if="currentStep < steps.length - 1"
|
|
class="neu-btn neu-btn--primary"
|
|
:disabled="!canProceed"
|
|
@click="nextStep"
|
|
>
|
|
{{ t('install.next') }}
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="neu-btn neu-btn--success"
|
|
:disabled="installing"
|
|
@click="finalize"
|
|
>
|
|
<span v-if="installing" class="neu-loading">⟳</span>
|
|
{{ t('install.finish') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth.store'
|
|
|
|
const { t } = useI18n()
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
|
|
const currentStep = ref(0)
|
|
const detectedURL = ref('')
|
|
const testingSSH = ref(false)
|
|
const installing = ref(false)
|
|
const error = ref('')
|
|
const sshTestResult = ref<{ success: boolean; message: string } | null>(null)
|
|
|
|
const steps = [
|
|
{ label: 'install.step1.label' },
|
|
{ label: 'install.step2.label' },
|
|
{ label: 'install.step3.label' },
|
|
{ label: 'install.step4.label' },
|
|
]
|
|
|
|
const form = ref({
|
|
instanceName: 'ProxmoxPanel',
|
|
publicUrl: '',
|
|
defaultLang: 'fr',
|
|
sshHost: '10.0.0.1:2244',
|
|
sshUsername: 'enzo',
|
|
sshPassword: '',
|
|
proxmoxUrl: 'https://10.0.0.1:8006',
|
|
proxmoxTokenId: '',
|
|
proxmoxTokenSecret: '',
|
|
})
|
|
|
|
const canProceed = computed(() => {
|
|
switch (currentStep.value) {
|
|
case 0: return !!form.value.instanceName
|
|
case 1: return sshTestResult.value?.success === true
|
|
case 2: return true // Token Proxmox optionnel
|
|
default: return true
|
|
}
|
|
})
|
|
|
|
onMounted(async () => {
|
|
// Récupérer les valeurs pré-remplies depuis l'API
|
|
try {
|
|
const res = await fetch('/api/install/status')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
detectedURL.value = data.detected_url || window.location.origin
|
|
form.value.publicUrl = detectedURL.value
|
|
}
|
|
} catch {
|
|
detectedURL.value = window.location.origin
|
|
form.value.publicUrl = window.location.origin
|
|
}
|
|
})
|
|
|
|
async function testSSH() {
|
|
testingSSH.value = true
|
|
sshTestResult.value = null
|
|
|
|
try {
|
|
const res = await fetch('/api/install/test-ssh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
host: form.value.sshHost,
|
|
username: form.value.sshUsername,
|
|
password: form.value.sshPassword,
|
|
}),
|
|
})
|
|
const data = await res.json()
|
|
sshTestResult.value = {
|
|
success: data.success,
|
|
message: data.success ? t('install.sshSuccess') : (data.error || t('install.sshFailed')),
|
|
}
|
|
} catch (e) {
|
|
sshTestResult.value = { success: false, message: t('install.networkError') }
|
|
} finally {
|
|
testingSSH.value = false
|
|
}
|
|
}
|
|
|
|
function nextStep() {
|
|
if (currentStep.value < steps.length - 1) {
|
|
currentStep.value++
|
|
sshTestResult.value = null
|
|
error.value = ''
|
|
}
|
|
}
|
|
|
|
async function finalize() {
|
|
installing.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const res = await fetch('/api/install/configure', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
instance_name: form.value.instanceName,
|
|
public_url: form.value.publicUrl || detectedURL.value,
|
|
default_lang: form.value.defaultLang,
|
|
ssh_host: form.value.sshHost,
|
|
ssh_username: form.value.sshUsername,
|
|
ssh_password: form.value.sshPassword,
|
|
proxmox_url: form.value.proxmoxUrl,
|
|
proxmox_token: (form.value.proxmoxTokenId && form.value.proxmoxTokenSecret)
|
|
? `PVEAPIToken=${form.value.proxmoxTokenId}=${form.value.proxmoxTokenSecret}`
|
|
: '',
|
|
}),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json()
|
|
error.value = data.error || t('install.error')
|
|
return
|
|
}
|
|
|
|
// Marquer comme installé et rediriger vers le login
|
|
authStore.isInstalled = true
|
|
localStorage.setItem('pxp_instance_name', form.value.instanceName)
|
|
router.push('/login')
|
|
} catch (e) {
|
|
error.value = t('install.networkError')
|
|
} finally {
|
|
installing.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.install-page {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--neu-bg);
|
|
padding: var(--neu-space-lg);
|
|
}
|
|
|
|
.install-container {
|
|
width: 100%;
|
|
max-width: 520px;
|
|
}
|
|
|
|
.install-header {
|
|
text-align: center;
|
|
margin-bottom: var(--neu-space-xl);
|
|
}
|
|
|
|
.install-logo {
|
|
width: 64px;
|
|
height: 64px;
|
|
background: var(--neu-primary);
|
|
color: #fff;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
border-radius: var(--neu-radius-lg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto var(--neu-space-md);
|
|
box-shadow: 4px 4px 12px rgba(108, 142, 244, 0.4);
|
|
}
|
|
|
|
.install-header h1 {
|
|
font-size: var(--neu-font-2xl);
|
|
color: var(--neu-text);
|
|
margin-bottom: var(--neu-space-xs);
|
|
}
|
|
|
|
.install-subtitle {
|
|
color: var(--neu-text-muted);
|
|
font-size: var(--neu-font-md);
|
|
}
|
|
|
|
.install-steps {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: var(--neu-space-lg);
|
|
position: relative;
|
|
}
|
|
|
|
.install-steps::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 14px;
|
|
left: 14px;
|
|
right: 14px;
|
|
height: 2px;
|
|
background: var(--neu-border);
|
|
z-index: 0;
|
|
}
|
|
|
|
.install-step {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.install-step__dot {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
background: var(--neu-surface);
|
|
border: 2px solid var(--neu-border);
|
|
color: var(--neu-text-muted);
|
|
transition: var(--neu-transition);
|
|
}
|
|
|
|
.install-step--active .install-step__dot {
|
|
border-color: var(--neu-primary);
|
|
color: var(--neu-primary);
|
|
}
|
|
|
|
.install-step--done .install-step__dot {
|
|
background: var(--neu-success);
|
|
border-color: var(--neu-success);
|
|
color: #fff;
|
|
}
|
|
|
|
.install-step__label {
|
|
font-size: var(--neu-font-xs);
|
|
color: var(--neu-text-muted);
|
|
}
|
|
|
|
.install-step--active .install-step__label {
|
|
color: var(--neu-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.install-card {
|
|
padding: var(--neu-space-xl);
|
|
}
|
|
|
|
.install-card h2 {
|
|
font-size: var(--neu-font-xl);
|
|
color: var(--neu-text);
|
|
margin-bottom: var(--neu-space-xs);
|
|
}
|
|
|
|
.step-desc {
|
|
color: var(--neu-text-muted);
|
|
margin-bottom: var(--neu-space-lg);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--neu-space-md);
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-size: var(--neu-font-sm);
|
|
font-weight: 600;
|
|
color: var(--neu-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: var(--neu-space-xs);
|
|
}
|
|
|
|
.form-group small {
|
|
display: block;
|
|
font-size: var(--neu-font-xs);
|
|
color: var(--neu-text-muted);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.install-result {
|
|
margin: var(--neu-space-md) 0;
|
|
padding: var(--neu-space-sm) var(--neu-space-md);
|
|
border-radius: var(--neu-radius-md);
|
|
font-size: var(--neu-font-sm);
|
|
}
|
|
|
|
.install-result--success {
|
|
background: rgba(76, 187, 138, 0.1);
|
|
color: var(--neu-success);
|
|
border: 1px solid rgba(76, 187, 138, 0.3);
|
|
}
|
|
|
|
.install-result--error {
|
|
background: rgba(240, 92, 107, 0.1);
|
|
color: var(--neu-danger);
|
|
border: 1px solid rgba(240, 92, 107, 0.3);
|
|
}
|
|
|
|
.install-summary {
|
|
background: var(--neu-bg);
|
|
border-radius: var(--neu-radius-md);
|
|
padding: var(--neu-space-md);
|
|
margin-bottom: var(--neu-space-lg);
|
|
}
|
|
|
|
.summary-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: var(--neu-space-xs) 0;
|
|
border-bottom: 1px solid var(--neu-border);
|
|
font-size: var(--neu-font-sm);
|
|
}
|
|
|
|
.summary-item:last-child { border-bottom: none; }
|
|
|
|
.summary-key { color: var(--neu-text-muted); }
|
|
.summary-value { color: var(--neu-text); font-weight: 500; }
|
|
|
|
.install-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-top: var(--neu-space-xl);
|
|
gap: var(--neu-space-sm);
|
|
}
|
|
|
|
.spacer { flex: 1; }
|
|
</style>
|