Add admin component (#6928)
* init version * relocate * add s3 bucket link * refactor handlers into weed/admin folder * fix login logout * adding favicon * remove fall back to http get topology * grpc dial option, disk total capacity * show filer count * fix each volume disk usage * add filers to dashboard * adding hosts, volumes, collections * refactor code and menu * remove "refresh" button * fix data for collections * rename cluster hosts into volume servers * add masters, filers * reorder * adding file browser * create folder and upload files * add filer version, created at time * remove mock data * remove fields * fix submenu item highlighting * fix bucket creation * purge files * delete multiple * fix bucket creation * remove region from buckets * add object store with buckets and users * rendering permission * refactor * get bucket objects and size * link to file browser * add file size and count for collections page * paginate the volumes * fix possible SSRF https://github.com/seaweedfs/seaweedfs/pull/6928/checks?check_run_id=45108469801 * Update weed/command/admin.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update weed/command/admin.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix build * import * remove filer CLI option * remove filer option * remove CLI options --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
320
weed/admin/handlers/handlers.go
Normal file
320
weed/admin/handlers/handlers.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
)
|
||||
|
||||
// AdminHandlers contains all the HTTP handlers for the admin interface
|
||||
type AdminHandlers struct {
|
||||
adminServer *dash.AdminServer
|
||||
authHandlers *AuthHandlers
|
||||
clusterHandlers *ClusterHandlers
|
||||
fileBrowserHandlers *FileBrowserHandlers
|
||||
}
|
||||
|
||||
// NewAdminHandlers creates a new instance of AdminHandlers
|
||||
func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
authHandlers := NewAuthHandlers(adminServer)
|
||||
clusterHandlers := NewClusterHandlers(adminServer)
|
||||
fileBrowserHandlers := NewFileBrowserHandlers(adminServer)
|
||||
return &AdminHandlers{
|
||||
adminServer: adminServer,
|
||||
authHandlers: authHandlers,
|
||||
clusterHandlers: clusterHandlers,
|
||||
fileBrowserHandlers: fileBrowserHandlers,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupRoutes configures all the routes for the admin interface
|
||||
func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, password string) {
|
||||
// Health check (no auth required)
|
||||
r.GET("/health", h.HealthCheck)
|
||||
|
||||
if authRequired {
|
||||
// Authentication routes (no auth required)
|
||||
r.GET("/login", h.authHandlers.ShowLogin)
|
||||
r.POST("/login", h.authHandlers.HandleLogin(username, password))
|
||||
r.GET("/logout", h.authHandlers.HandleLogout)
|
||||
|
||||
// Protected routes group
|
||||
protected := r.Group("/")
|
||||
protected.Use(dash.RequireAuth())
|
||||
|
||||
// Main admin interface routes
|
||||
protected.GET("/", h.ShowDashboard)
|
||||
protected.GET("/admin", h.ShowDashboard)
|
||||
|
||||
// Object Store management routes
|
||||
protected.GET("/object-store/buckets", h.ShowS3Buckets)
|
||||
protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails)
|
||||
protected.GET("/object-store/users", h.ShowObjectStoreUsers)
|
||||
|
||||
// File browser routes
|
||||
protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser)
|
||||
|
||||
// Cluster management routes
|
||||
protected.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters)
|
||||
protected.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers)
|
||||
protected.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers)
|
||||
protected.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes)
|
||||
protected.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections)
|
||||
|
||||
// API routes for AJAX calls
|
||||
api := protected.Group("/api")
|
||||
{
|
||||
api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology)
|
||||
api.GET("/cluster/masters", h.clusterHandlers.GetMasters)
|
||||
api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers)
|
||||
api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data
|
||||
|
||||
// S3 API routes
|
||||
s3Api := api.Group("/s3")
|
||||
{
|
||||
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI)
|
||||
s3Api.POST("/buckets", h.adminServer.CreateBucket)
|
||||
s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket)
|
||||
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails)
|
||||
}
|
||||
|
||||
// File management API routes
|
||||
filesApi := api.Group("/files")
|
||||
{
|
||||
filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile)
|
||||
filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles)
|
||||
filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder)
|
||||
filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No authentication required - all routes are public
|
||||
r.GET("/", h.ShowDashboard)
|
||||
r.GET("/admin", h.ShowDashboard)
|
||||
|
||||
// Object Store management routes
|
||||
r.GET("/object-store/buckets", h.ShowS3Buckets)
|
||||
r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails)
|
||||
r.GET("/object-store/users", h.ShowObjectStoreUsers)
|
||||
|
||||
// File browser routes
|
||||
r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser)
|
||||
|
||||
// Cluster management routes
|
||||
r.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters)
|
||||
r.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers)
|
||||
r.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers)
|
||||
r.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes)
|
||||
r.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections)
|
||||
|
||||
// API routes for AJAX calls
|
||||
api := r.Group("/api")
|
||||
{
|
||||
api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology)
|
||||
api.GET("/cluster/masters", h.clusterHandlers.GetMasters)
|
||||
api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers)
|
||||
api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data
|
||||
|
||||
// S3 API routes
|
||||
s3Api := api.Group("/s3")
|
||||
{
|
||||
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI)
|
||||
s3Api.POST("/buckets", h.adminServer.CreateBucket)
|
||||
s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket)
|
||||
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails)
|
||||
}
|
||||
|
||||
// File management API routes
|
||||
filesApi := api.Group("/files")
|
||||
{
|
||||
filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile)
|
||||
filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles)
|
||||
filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder)
|
||||
filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HealthCheck returns the health status of the admin interface
|
||||
func (h *AdminHandlers) HealthCheck(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
// ShowDashboard renders the main admin dashboard
|
||||
func (h *AdminHandlers) ShowDashboard(c *gin.Context) {
|
||||
// Get admin data from the server
|
||||
adminData := h.getAdminData(c)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
adminComponent := app.Admin(adminData)
|
||||
layoutComponent := layout.Layout(c, adminComponent)
|
||||
err := layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3Buckets renders the S3 buckets management page
|
||||
func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) {
|
||||
// Get S3 buckets data from the server
|
||||
s3Data := h.getS3BucketsData(c)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
s3Component := app.S3Buckets(s3Data)
|
||||
layoutComponent := layout.Layout(c, s3Component)
|
||||
err := layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowBucketDetails returns detailed information about a specific bucket
|
||||
func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) {
|
||||
bucketName := c.Param("bucket")
|
||||
details, err := h.adminServer.GetBucketDetails(bucketName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, details)
|
||||
}
|
||||
|
||||
// ShowObjectStoreUsers renders the object store users management page
|
||||
func (h *AdminHandlers) ShowObjectStoreUsers(c *gin.Context) {
|
||||
// Get object store users data from the server
|
||||
usersData := h.getObjectStoreUsersData(c)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
usersComponent := app.ObjectStoreUsers(usersData)
|
||||
layoutComponent := layout.Layout(c, usersComponent)
|
||||
err := layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// getS3BucketsData retrieves S3 buckets data from the server
|
||||
func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
// Get S3 buckets
|
||||
buckets, err := h.adminServer.GetS3Buckets()
|
||||
if err != nil {
|
||||
// Return empty data on error
|
||||
return dash.S3BucketsData{
|
||||
Username: username,
|
||||
Buckets: []dash.S3Bucket{},
|
||||
TotalBuckets: 0,
|
||||
TotalSize: 0,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
var totalSize int64
|
||||
for _, bucket := range buckets {
|
||||
totalSize += bucket.Size
|
||||
}
|
||||
|
||||
return dash.S3BucketsData{
|
||||
Username: username,
|
||||
Buckets: buckets,
|
||||
TotalBuckets: len(buckets),
|
||||
TotalSize: totalSize,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// getObjectStoreUsersData retrieves object store users data from the server
|
||||
func (h *AdminHandlers) getObjectStoreUsersData(c *gin.Context) dash.ObjectStoreUsersData {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
// Get object store users
|
||||
users, err := h.adminServer.GetObjectStoreUsers()
|
||||
if err != nil {
|
||||
// Return empty data on error
|
||||
return dash.ObjectStoreUsersData{
|
||||
Username: username,
|
||||
Users: []dash.ObjectStoreUser{},
|
||||
TotalUsers: 0,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
return dash.ObjectStoreUsersData{
|
||||
Username: username,
|
||||
Users: users,
|
||||
TotalUsers: len(users),
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// getAdminData retrieves admin data from the server (now uses consolidated method)
|
||||
func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData {
|
||||
username := c.GetString("username")
|
||||
|
||||
// Use the consolidated GetAdminData method from AdminServer
|
||||
adminData, err := h.adminServer.GetAdminData(username)
|
||||
if err != nil {
|
||||
// Return default data when services are not available
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
masterNodes := []dash.MasterNode{
|
||||
{
|
||||
Address: "localhost:9333",
|
||||
IsLeader: true,
|
||||
Status: "unreachable",
|
||||
},
|
||||
}
|
||||
|
||||
return dash.AdminData{
|
||||
Username: username,
|
||||
ClusterStatus: "warning",
|
||||
TotalVolumes: 0,
|
||||
TotalFiles: 0,
|
||||
TotalSize: 0,
|
||||
MasterNodes: masterNodes,
|
||||
VolumeServers: []dash.VolumeServer{},
|
||||
FilerNodes: []dash.FilerNode{},
|
||||
DataCenters: []dash.DataCenter{},
|
||||
LastUpdated: time.Now(),
|
||||
SystemHealth: "poor",
|
||||
}
|
||||
}
|
||||
|
||||
return adminData
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func (h *AdminHandlers) determineClusterStatus(topology *dash.ClusterTopology, masters []dash.MasterNode) string {
|
||||
if len(topology.VolumeServers) == 0 {
|
||||
return "warning"
|
||||
}
|
||||
return "healthy"
|
||||
}
|
||||
|
||||
func (h *AdminHandlers) determineSystemHealth(topology *dash.ClusterTopology, masters []dash.MasterNode) string {
|
||||
if len(topology.VolumeServers) > 0 && len(masters) > 0 {
|
||||
return "good"
|
||||
}
|
||||
return "fair"
|
||||
}
|
||||
Reference in New Issue
Block a user