feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ : - API REST + WebSocket (chi, gorilla/websocket) - Authentification PAM via SSH + JWT RS256 - Chiffrement AES-256-GCM pour secrets SQLite - Pool SSH, client Proxmox REST, hub WebSocket pub/sub - Système de modules compilés à initialisation conditionnelle - Audit log, migrations SQLite versionnées Frontend Vue 3 + Vite + TypeScript : - Thème Neumorphism sombre/clair (CSS custom properties) - Wizard d'installation, Dashboard drag-drop, Terminal xterm.js - Toutes les vues CORE + stubs modules optionnels - i18n EN/FR (vue-i18n v11) Infrastructure : - Docker multi-stage (Go → alpine, Node → nginx) - docker-compose.yml, .gitattributes, LICENSE MIT, README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
5dbcb1df07
66 changed files with 10370 additions and 0 deletions
480
frontend/src/views/Install.vue
Normal file
480
frontend/src/views/Install.vue
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
<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.proxmoxToken') }}</label>
|
||||
<input v-model="form.proxmoxToken" class="neu-input" placeholder="PVEAPIToken=enzo@pam!panel=xxxx" />
|
||||
<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',
|
||||
proxmoxToken: '',
|
||||
})
|
||||
|
||||
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.proxmoxToken,
|
||||
}),
|
||||
})
|
||||
|
||||
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue