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
180
frontend/src/stores/auth.store.ts
Normal file
180
frontend/src/stores/auth.store.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// Store d'authentification — gère la session JWT, le profil utilisateur et l'état d'installation.
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
is_admin: boolean
|
||||
lang: string
|
||||
theme: string
|
||||
sidebar_position: string
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// État
|
||||
const user = ref<User | null>(null)
|
||||
const accessToken = ref<string | null>(localStorage.getItem('pxp_token'))
|
||||
const isInstalled = ref(false)
|
||||
const installChecked = ref(false)
|
||||
|
||||
// Computed
|
||||
const isAuthenticated = computed(() => !!accessToken.value && !!user.value)
|
||||
|
||||
// ── Actions ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Vérifie si l'application est installée via l'API.
|
||||
* Appelé une seule fois au démarrage par le router guard.
|
||||
*/
|
||||
async function checkInstallation(): Promise<void> {
|
||||
try {
|
||||
const res = await fetch('/api/install/check')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
isInstalled.value = data.installed
|
||||
}
|
||||
} catch {
|
||||
// En cas d'erreur réseau, on suppose installé pour éviter une boucle
|
||||
isInstalled.value = true
|
||||
} finally {
|
||||
installChecked.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentifie l'utilisateur avec ses credentials Linux.
|
||||
*/
|
||||
async function login(username: string, password: string): Promise<void> {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.error || 'Erreur d\'authentification')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
accessToken.value = data.access_token
|
||||
localStorage.setItem('pxp_token', data.access_token)
|
||||
user.value = data.user
|
||||
|
||||
// Planifier le renouvellement automatique avant expiration (14 min)
|
||||
scheduleRefresh(14 * 60 * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tente de renouveler le token via le cookie httpOnly (pxp_refresh).
|
||||
* Appelé au démarrage de l'application.
|
||||
*/
|
||||
async function tryRefresh(): Promise<void> {
|
||||
const token = localStorage.getItem('pxp_token')
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
credentials: 'include', // Inclure le cookie httpOnly
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
accessToken.value = data.access_token
|
||||
localStorage.setItem('pxp_token', data.access_token)
|
||||
|
||||
// Charger le profil utilisateur
|
||||
await fetchMe()
|
||||
scheduleRefresh(14 * 60 * 1000)
|
||||
} else {
|
||||
// Refresh échoué — nettoyer la session
|
||||
clearSession()
|
||||
}
|
||||
} catch {
|
||||
clearSession()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le profil de l'utilisateur connecté.
|
||||
*/
|
||||
async function fetchMe(): Promise<void> {
|
||||
if (!accessToken.value) return
|
||||
|
||||
const res = await fetch('/api/auth/me', {
|
||||
headers: { Authorization: `Bearer ${accessToken.value}` },
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
user.value = await res.json()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Déconnecte l'utilisateur.
|
||||
*/
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken.value}` },
|
||||
credentials: 'include',
|
||||
})
|
||||
} finally {
|
||||
clearSession()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les préférences de l'utilisateur (thème, langue, sidebar).
|
||||
*/
|
||||
async function updatePreferences(prefs: Partial<Pick<User, 'lang' | 'theme' | 'sidebar_position'>>): Promise<void> {
|
||||
if (!accessToken.value) return
|
||||
|
||||
await fetch('/api/auth/preferences', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken.value}`,
|
||||
},
|
||||
body: JSON.stringify(prefs),
|
||||
})
|
||||
|
||||
// Mettre à jour localement
|
||||
if (user.value) {
|
||||
Object.assign(user.value, prefs)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers privés ────────────────────────────────────────────────────────
|
||||
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function scheduleRefresh(delayMs: number): void {
|
||||
if (refreshTimer) clearTimeout(refreshTimer)
|
||||
refreshTimer = setTimeout(() => tryRefresh(), delayMs)
|
||||
}
|
||||
|
||||
function clearSession(): void {
|
||||
user.value = null
|
||||
accessToken.value = null
|
||||
localStorage.removeItem('pxp_token')
|
||||
if (refreshTimer) clearTimeout(refreshTimer)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
isInstalled,
|
||||
installChecked,
|
||||
isAuthenticated,
|
||||
checkInstallation,
|
||||
login,
|
||||
logout,
|
||||
tryRefresh,
|
||||
fetchMe,
|
||||
updatePreferences,
|
||||
}
|
||||
})
|
||||
84
frontend/src/stores/ui.store.ts
Normal file
84
frontend/src/stores/ui.store.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Store UI — gère le thème (dark/light) et la position de la sidebar.
|
||||
// Les préférences sont persistées localement et synchronisées avec le serveur.
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
export type SidebarPosition = 'left' | 'right'
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const theme = ref<Theme>('dark')
|
||||
const sidebarPosition = ref<SidebarPosition>('left')
|
||||
const sidebarCollapsed = ref(false)
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
/**
|
||||
* Initialise le thème depuis les préférences locales.
|
||||
* Appelé au montage de App.vue.
|
||||
*/
|
||||
function initTheme(): void {
|
||||
const savedTheme = localStorage.getItem('pxp_theme') as Theme | null
|
||||
const savedSidebar = localStorage.getItem('pxp_sidebar') as SidebarPosition | null
|
||||
|
||||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||||
theme.value = savedTheme
|
||||
}
|
||||
if (savedSidebar === 'left' || savedSidebar === 'right') {
|
||||
sidebarPosition.value = savedSidebar
|
||||
}
|
||||
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bascule entre thème sombre et clair.
|
||||
*/
|
||||
function toggleTheme(): void {
|
||||
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
||||
localStorage.setItem('pxp_theme', theme.value)
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le thème explicitement.
|
||||
*/
|
||||
function setTheme(newTheme: Theme): void {
|
||||
theme.value = newTheme
|
||||
localStorage.setItem('pxp_theme', newTheme)
|
||||
applyTheme(newTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la position de la sidebar.
|
||||
*/
|
||||
function setSidebarPosition(pos: SidebarPosition): void {
|
||||
sidebarPosition.value = pos
|
||||
localStorage.setItem('pxp_sidebar', pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bascule l'état réduit de la sidebar.
|
||||
*/
|
||||
function toggleSidebarCollapse(): void {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique le thème sur l'élément <html> via data-theme.
|
||||
*/
|
||||
function applyTheme(t: Theme): void {
|
||||
document.documentElement.setAttribute('data-theme', t)
|
||||
}
|
||||
|
||||
return {
|
||||
theme,
|
||||
sidebarPosition,
|
||||
sidebarCollapsed,
|
||||
mobileMenuOpen,
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setTheme,
|
||||
setSidebarPosition,
|
||||
toggleSidebarCollapse,
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue