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:
enzo 2026-03-20 21:08:53 +01:00
commit 5dbcb1df07
66 changed files with 10370 additions and 0 deletions

View 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>