Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ func (d *dummyClient) UpdateVolume(poolName string, volumeName string, config co
return fmt.Errorf("not implemented in dummy client")
}

func (d *dummyClient) DeleteVolume(poolName string, volumeName string) error {
return fmt.Errorf("not implemented in dummy client")
}

func (d *dummyClient) UpdateNetwork(name string, bridgeName string) error {
return fmt.Errorf("not implemented in dummy client")
}
Expand Down
1 change: 1 addition & 0 deletions pkg/libvirtclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type ClientInterface interface {

// Storage operations
UpdateVolume(poolName string, volumeName string, config core.VolumeConfig) error
DeleteVolume(poolName string, volumeName string) error

// Network operations
UpdateNetwork(name string, bridgeName string) error
Expand Down
22 changes: 22 additions & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,28 @@ func (s *Server) handleUpdateVolume() http.HandlerFunc {
}
}

func (s *Server) handleDeleteVolume() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
poolName := chi.URLParam(r, "poolName")
volumeName := chi.URLParam(r, "volumeName")

if poolName == "" || volumeName == "" {
http.Error(w, `{"error": "Pool name and volume name are required"}`, http.StatusBadRequest)
return
}

err := s.client.DeleteVolume(poolName, volumeName)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "Failed to delete volume: %s"}`, err.Error()), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}
}

func (s *Server) handleCreateNetwork() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Expand Down
25 changes: 13 additions & 12 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (s *Server) webAuthMiddleware(next http.Handler) http.Handler {
http.Error(w, "Failed to create session", http.StatusInternalServerError)
return
}

// Set secure session cookie
http.SetCookie(w, &http.Cookie{
Name: "flint_session",
Expand Down Expand Up @@ -309,7 +309,7 @@ func (s *Server) validatePassphrase(passphrase string) bool {
}

storedHash := s.getStoredPassphraseHash()

// Check if it's an old SHA256 hash (64 hex chars) and migrate to bcrypt
if len(storedHash) == 64 {
// Legacy SHA256 hash - compare directly for backward compatibility
Expand Down Expand Up @@ -454,7 +454,7 @@ func min(a, b int) int {
func (s *Server) setupRoutes() {
// Public API endpoints (no authentication required)
s.router.Get("/api/health", s.handleHealthCheck())

// Serial console endpoints (token-based auth, not middleware auth)
s.router.Get("/api/vms/{uuid}/serial-console", s.handleGetVMSerialConsole())
s.router.Get("/api/vms/{uuid}/serial-console/ws", s.handleVMSerialConsoleWS())
Expand Down Expand Up @@ -494,11 +494,12 @@ func (s *Server) setupRoutes() {
r.Post("/bridges", s.handleCreateBridge())
r.Put("/networks/{networkName}", s.handleUpdateNetwork())
r.Delete("/networks/{networkName}", s.handleDeleteNetwork())
r.Delete("/storage-pools/{poolName}/volumes/{volumeName}", s.handleDeleteVolume())
r.Get("/images", s.handleGetImages())
r.Post("/images/import-from-path", s.handleImportImageFromPath())
r.Post("/images/download", s.handleDownloadImage())
r.Delete("/images/{imageId}", s.handleDeleteImage())

// Image repository endpoints
r.Get("/image-repository", s.handleGetRepositoryImages())
r.Post("/image-repository/{imageId}/download", s.handleDownloadRepositoryImage())
Expand Down Expand Up @@ -670,19 +671,19 @@ func (s *Server) generateSessionID() (string, error) {
func (s *Server) isValidSession(sessionID string) bool {
s.sessionsMu.RLock()
defer s.sessionsMu.RUnlock()

expiry, exists := s.sessions[sessionID]
if !exists {
return false
}

// Check if session has expired
if time.Now().After(expiry) {
// Clean up expired session
go s.cleanupExpiredSession(sessionID)
return false
}

return true
}

Expand All @@ -692,19 +693,19 @@ func (s *Server) createSession() (string, error) {
if err != nil {
return "", err
}

s.sessionsMu.Lock()
defer s.sessionsMu.Unlock()

// Initialize sessions map if needed
if s.sessions == nil {
s.sessions = make(map[string]time.Time)
}

// Session expires in 24 hours
expiry := time.Now().Add(24 * time.Hour)
s.sessions[sessionID] = expiry

return sessionID, nil
}

Expand All @@ -719,7 +720,7 @@ func (s *Server) cleanupExpiredSession(sessionID string) {
func (s *Server) cleanupExpiredSessions() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()

for {
select {
case <-ticker.C:
Expand Down
10 changes: 6 additions & 4 deletions web/app/images/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { useState, useEffect } from "react"
import { useTranslation } from "@/components/i18n-provider"
import { AppShell } from "@/components/app-shell"
import { ImagesView } from "@/components/images-view"
import { ImageRepository } from "@/components/image-repository"
Expand All @@ -9,6 +10,7 @@ import { HardDrive, Cloud } from "lucide-react"
import { SPACING, TYPOGRAPHY } from "@/lib/ui-constants"

export default function ImagesPage() {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState("my-images")

// Handle URL hash for direct navigation
Expand All @@ -33,20 +35,20 @@ export default function ImagesPage() {
{/* Page Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<h1 className={TYPOGRAPHY.pageTitle}>Images</h1>
<p className="text-muted-foreground">Manage virtual machine images and cloud repository</p>
<h1 className={TYPOGRAPHY.pageTitle}>{t('images.title')}</h1>
<p className="text-muted-foreground">{t('images.subtitle')}</p>
</div>
</div>

<Tabs value={activeTab} onValueChange={handleTabChange} className={SPACING.section}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="my-images" className="flex items-center gap-2">
<HardDrive className="h-4 w-4" />
My Images
{t('images.myImages')}
</TabsTrigger>
<TabsTrigger value="repository" className="flex items-center gap-2">
<Cloud className="h-4 w-4" />
Cloud Repository
{t('images.cloudRepository')}
</TabsTrigger>
</TabsList>

Expand Down
21 changes: 12 additions & 9 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Inter } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import { I18nProvider } from "@/components/i18n-provider";
import { Toaster } from "@/components/ui/toaster";
import { Suspense } from "react";
import "./globals.css"
Expand Down Expand Up @@ -60,15 +61,17 @@ export default function RootLayout({
</div>
</div>
}>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange={false} // Enable smooth theme transitions
>
{children}
<Toaster />
</ThemeProvider>
<I18nProvider>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange={false} // Enable smooth theme transitions
>
{children}
<Toaster />
</ThemeProvider>
</I18nProvider>
</Suspense>
</body>
</html>
Expand Down
18 changes: 10 additions & 8 deletions web/app/vms/console/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,32 @@ import { PageLayout } from '@/components/shared/page-layout';
import { getUrlParams, navigateTo, routes } from '@/lib/navigation';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
import { useTranslation } from '@/components/i18n-provider';

const VMSerialConsole = dynamic(
() => import('@/components/vm-serial-console').then(mod => mod.VMSerialConsole),
{ ssr: false }
);

export default function ConsolePage() {
const { t } = useTranslation();
const searchParams = getUrlParams();

const vmUuid = searchParams.get('id'); // ✅ extract vmUuid safely

if (!vmUuid) {
return (
<PageLayout
title="Serial Console"
description="VM ID is required to access the console"
title={t('vm.serialConsole')}
description={t('vm.vmIdRequired')}
>
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<h2 className="text-xl font-semibold mb-2">No VM Selected</h2>
<p className="text-muted-foreground mb-4">Please select a VM to access its console</p>
<h2 className="text-xl font-semibold mb-2">{t('vm.noVMSelected')}</h2>
<p className="text-muted-foreground mb-4">{t('vm.selectVMForConsole')}</p>
<Button onClick={() => navigateTo('/vms')}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to VMs
{t('vm.backToVMs')}
</Button>
</div>
</div>
Expand All @@ -37,15 +39,15 @@ export default function ConsolePage() {

return (
<PageLayout
title="Serial Console"
description={`Connect to VM ${vmUuid.slice(0, 8)}... serial console for direct access`}
title={t('vm.serialConsole')}
description={`${t('vm.connectToSerialConsole')} ${vmUuid.slice(0, 8)}...`}
actions={
<Button
variant="outline"
onClick={() => navigateTo(routes.vmDetail(vmUuid))}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to VM Details
{t('vm.backToVMDetails')}
</Button>
}
>
Expand Down
9 changes: 6 additions & 3 deletions web/app/vms/detail/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
import dynamic from 'next/dynamic';
import { PageLayout } from '@/components/shared/page-layout';
import { ErrorBoundary } from '@/components/error-boundary';
import { useTranslation } from '@/components/i18n-provider';

const VMDetailView = dynamic(
() => import('@/components/vm-detail-view').then(mod => mod.VMDetailView),
() => import('@/components/vm-detail-view'),
{ ssr: false }
);

export default function VMDetailPage() {
const { t } = useTranslation();

return (
<PageLayout
title="Virtual Machine Details"
description="View and manage virtual machine configuration and performance"
title={t('vm.details')}
description={t('vm.manageFleet')}
>
<ErrorBoundary>
<VMDetailView />
Expand Down
Loading