Admin UI: replace gin with mux (#8420)
* Replace admin gin router with mux * Update layout_templ.go * Harden admin handlers * Add login CSRF handling * Fix filer copy naming conflict * address comments * address comments
This commit is contained in:
@@ -5,7 +5,8 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
// AdminHandlers contains all the HTTP handlers for the admin interface
|
||||
type AdminHandlers struct {
|
||||
adminServer *dash.AdminServer
|
||||
sessionStore sessions.Store
|
||||
authHandlers *AuthHandlers
|
||||
clusterHandlers *ClusterHandlers
|
||||
fileBrowserHandlers *FileBrowserHandlers
|
||||
@@ -29,8 +31,8 @@ type AdminHandlers struct {
|
||||
}
|
||||
|
||||
// NewAdminHandlers creates a new instance of AdminHandlers
|
||||
func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
authHandlers := NewAuthHandlers(adminServer)
|
||||
func NewAdminHandlers(adminServer *dash.AdminServer, store sessions.Store) *AdminHandlers {
|
||||
authHandlers := NewAuthHandlers(adminServer, store)
|
||||
clusterHandlers := NewClusterHandlers(adminServer)
|
||||
fileBrowserHandlers := NewFileBrowserHandlers(adminServer)
|
||||
userHandlers := NewUserHandlers(adminServer)
|
||||
@@ -40,6 +42,7 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
serviceAccountHandlers := NewServiceAccountHandlers(adminServer)
|
||||
return &AdminHandlers{
|
||||
adminServer: adminServer,
|
||||
sessionStore: store,
|
||||
authHandlers: authHandlers,
|
||||
clusterHandlers: clusterHandlers,
|
||||
fileBrowserHandlers: fileBrowserHandlers,
|
||||
@@ -52,17 +55,17 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
}
|
||||
|
||||
// SetupRoutes configures all the routes for the admin interface
|
||||
func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, adminPassword, readOnlyUser, readOnlyPassword string, enableUI bool) {
|
||||
func (h *AdminHandlers) SetupRoutes(r *mux.Router, authRequired bool, adminUser, adminPassword, readOnlyUser, readOnlyPassword string, enableUI bool) {
|
||||
// Health check (no auth required)
|
||||
r.GET("/health", h.HealthCheck)
|
||||
r.HandleFunc("/health", h.HealthCheck).Methods(http.MethodGet)
|
||||
|
||||
// Prometheus metrics endpoint (no auth required)
|
||||
r.GET("/metrics", gin.WrapH(promhttp.HandlerFor(stats.Gather, promhttp.HandlerOpts{})))
|
||||
r.Handle("/metrics", promhttp.HandlerFor(stats.Gather, promhttp.HandlerOpts{})).Methods(http.MethodGet)
|
||||
|
||||
// Favicon route (no auth required) - redirect to static version
|
||||
r.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/static/favicon.ico")
|
||||
})
|
||||
r.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, "/static/favicon.ico", http.StatusMovedPermanently)
|
||||
}).Methods(http.MethodGet)
|
||||
|
||||
// Skip UI routes if UI is not enabled
|
||||
if !enableUI {
|
||||
@@ -71,499 +74,321 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
|
||||
if authRequired {
|
||||
// Authentication routes (no auth required)
|
||||
r.GET("/login", h.authHandlers.ShowLogin)
|
||||
r.POST("/login", h.authHandlers.HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword))
|
||||
r.GET("/logout", h.authHandlers.HandleLogout)
|
||||
r.HandleFunc("/login", h.authHandlers.ShowLogin).Methods(http.MethodGet)
|
||||
r.Handle("/login", h.authHandlers.HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/logout", h.authHandlers.HandleLogout).Methods(http.MethodGet)
|
||||
|
||||
// Protected routes group
|
||||
protected := r.Group("/")
|
||||
protected.Use(dash.RequireAuth())
|
||||
protected := r.NewRoute().Subrouter()
|
||||
protected.Use(dash.RequireAuth(h.sessionStore))
|
||||
h.registerUIRoutes(protected)
|
||||
|
||||
// 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.userHandlers.ShowObjectStoreUsers)
|
||||
protected.GET("/object-store/policies", h.policyHandlers.ShowPolicies)
|
||||
protected.GET("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts)
|
||||
protected.GET("/object-store/s3tables/buckets", h.ShowS3TablesBuckets)
|
||||
protected.GET("/object-store/s3tables/buckets/:bucket/namespaces", h.ShowS3TablesNamespaces)
|
||||
protected.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables", h.ShowS3TablesTables)
|
||||
protected.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables/:table", h.ShowS3TablesTableDetails)
|
||||
protected.GET("/object-store/iceberg", h.ShowIcebergCatalog)
|
||||
protected.GET("/object-store/iceberg/:catalog/namespaces", h.ShowIcebergNamespaces)
|
||||
protected.GET("/object-store/iceberg/:catalog/namespaces/:namespace/tables", h.ShowIcebergTables)
|
||||
protected.GET("/object-store/iceberg/:catalog/namespaces/:namespace/tables/:table", h.ShowIcebergTableDetails)
|
||||
|
||||
// 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)
|
||||
|
||||
// Storage management routes
|
||||
protected.GET("/storage/volumes", h.clusterHandlers.ShowClusterVolumes)
|
||||
protected.GET("/storage/volumes/:id/:server", h.clusterHandlers.ShowVolumeDetails)
|
||||
protected.GET("/storage/collections", h.clusterHandlers.ShowClusterCollections)
|
||||
protected.GET("/storage/collections/:name", h.clusterHandlers.ShowCollectionDetails)
|
||||
protected.GET("/storage/ec-shards", h.clusterHandlers.ShowClusterEcShards)
|
||||
protected.GET("/storage/ec-volumes/:id", h.clusterHandlers.ShowEcVolumeDetails)
|
||||
|
||||
// Message Queue management routes
|
||||
protected.GET("/mq/brokers", h.mqHandlers.ShowBrokers)
|
||||
protected.GET("/mq/topics", h.mqHandlers.ShowTopics)
|
||||
protected.GET("/mq/topics/:namespace/:topic", h.mqHandlers.ShowTopicDetails)
|
||||
|
||||
protected.GET("/plugin", h.pluginHandlers.ShowPlugin)
|
||||
protected.GET("/plugin/configuration", h.pluginHandlers.ShowPluginConfiguration)
|
||||
protected.GET("/plugin/queue", h.pluginHandlers.ShowPluginQueue)
|
||||
protected.GET("/plugin/detection", h.pluginHandlers.ShowPluginDetection)
|
||||
protected.GET("/plugin/execution", h.pluginHandlers.ShowPluginExecution)
|
||||
protected.GET("/plugin/monitoring", h.pluginHandlers.ShowPluginMonitoring)
|
||||
|
||||
// API routes for AJAX calls
|
||||
api := r.Group("/api")
|
||||
api.Use(dash.RequireAuthAPI()) // Use API-specific auth middleware
|
||||
{
|
||||
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
|
||||
api.GET("/config", h.adminServer.GetConfigInfo) // Configuration information
|
||||
|
||||
// S3 API routes
|
||||
s3Api := api.Group("/s3")
|
||||
{
|
||||
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI)
|
||||
s3Api.POST("/buckets", dash.RequireWriteAccess(), h.adminServer.CreateBucket)
|
||||
s3Api.DELETE("/buckets/:bucket", dash.RequireWriteAccess(), h.adminServer.DeleteBucket)
|
||||
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails)
|
||||
s3Api.PUT("/buckets/:bucket/quota", dash.RequireWriteAccess(), h.adminServer.UpdateBucketQuota)
|
||||
s3Api.PUT("/buckets/:bucket/owner", dash.RequireWriteAccess(), h.adminServer.UpdateBucketOwner)
|
||||
}
|
||||
|
||||
// User management API routes
|
||||
usersApi := api.Group("/users")
|
||||
{
|
||||
usersApi.GET("", h.userHandlers.GetUsers)
|
||||
usersApi.POST("", dash.RequireWriteAccess(), h.userHandlers.CreateUser)
|
||||
usersApi.GET("/:username", h.userHandlers.GetUserDetails)
|
||||
usersApi.PUT("/:username", dash.RequireWriteAccess(), h.userHandlers.UpdateUser)
|
||||
usersApi.DELETE("/:username", dash.RequireWriteAccess(), h.userHandlers.DeleteUser)
|
||||
usersApi.POST("/:username/access-keys", dash.RequireWriteAccess(), h.userHandlers.CreateAccessKey)
|
||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", dash.RequireWriteAccess(), h.userHandlers.DeleteAccessKey)
|
||||
usersApi.PUT("/:username/access-keys/:accessKeyId/status", dash.RequireWriteAccess(), h.userHandlers.UpdateAccessKeyStatus)
|
||||
usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||
usersApi.PUT("/:username/policies", dash.RequireWriteAccess(), h.userHandlers.UpdateUserPolicies)
|
||||
}
|
||||
|
||||
// Service Account management API routes
|
||||
saApi := api.Group("/service-accounts")
|
||||
{
|
||||
saApi.GET("", h.serviceAccountHandlers.GetServiceAccounts)
|
||||
saApi.POST("", dash.RequireWriteAccess(), h.serviceAccountHandlers.CreateServiceAccount)
|
||||
saApi.GET("/:id", h.serviceAccountHandlers.GetServiceAccountDetails)
|
||||
saApi.PUT("/:id", dash.RequireWriteAccess(), h.serviceAccountHandlers.UpdateServiceAccount)
|
||||
saApi.DELETE("/:id", dash.RequireWriteAccess(), h.serviceAccountHandlers.DeleteServiceAccount)
|
||||
}
|
||||
|
||||
// Object Store Policy management API routes
|
||||
objectStorePoliciesApi := api.Group("/object-store/policies")
|
||||
{
|
||||
objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies)
|
||||
objectStorePoliciesApi.POST("", dash.RequireWriteAccess(), h.policyHandlers.CreatePolicy)
|
||||
objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy)
|
||||
objectStorePoliciesApi.PUT("/:name", dash.RequireWriteAccess(), h.policyHandlers.UpdatePolicy)
|
||||
objectStorePoliciesApi.DELETE("/:name", dash.RequireWriteAccess(), h.policyHandlers.DeletePolicy)
|
||||
objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy)
|
||||
}
|
||||
|
||||
// S3 Tables API routes
|
||||
s3TablesApi := api.Group("/s3tables")
|
||||
{
|
||||
s3TablesApi.GET("/buckets", h.adminServer.ListS3TablesBucketsAPI)
|
||||
s3TablesApi.POST("/buckets", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesBucket)
|
||||
s3TablesApi.DELETE("/buckets", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesBucket)
|
||||
s3TablesApi.GET("/namespaces", h.adminServer.ListS3TablesNamespacesAPI)
|
||||
s3TablesApi.POST("/namespaces", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesNamespace)
|
||||
s3TablesApi.DELETE("/namespaces", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesNamespace)
|
||||
s3TablesApi.GET("/tables", h.adminServer.ListS3TablesTablesAPI)
|
||||
s3TablesApi.POST("/tables", dash.RequireWriteAccess(), h.adminServer.CreateS3TablesTable)
|
||||
s3TablesApi.DELETE("/tables", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesTable)
|
||||
s3TablesApi.PUT("/bucket-policy", dash.RequireWriteAccess(), h.adminServer.PutS3TablesBucketPolicy)
|
||||
s3TablesApi.GET("/bucket-policy", h.adminServer.GetS3TablesBucketPolicy)
|
||||
s3TablesApi.DELETE("/bucket-policy", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesBucketPolicy)
|
||||
s3TablesApi.PUT("/table-policy", dash.RequireWriteAccess(), h.adminServer.PutS3TablesTablePolicy)
|
||||
s3TablesApi.GET("/table-policy", h.adminServer.GetS3TablesTablePolicy)
|
||||
s3TablesApi.DELETE("/table-policy", dash.RequireWriteAccess(), h.adminServer.DeleteS3TablesTablePolicy)
|
||||
s3TablesApi.PUT("/tags", dash.RequireWriteAccess(), h.adminServer.TagS3TablesResource)
|
||||
s3TablesApi.GET("/tags", h.adminServer.ListS3TablesTags)
|
||||
s3TablesApi.DELETE("/tags", dash.RequireWriteAccess(), h.adminServer.UntagS3TablesResource)
|
||||
}
|
||||
|
||||
// File management API routes
|
||||
filesApi := api.Group("/files")
|
||||
{
|
||||
filesApi.DELETE("/delete", dash.RequireWriteAccess(), h.fileBrowserHandlers.DeleteFile)
|
||||
filesApi.DELETE("/delete-multiple", dash.RequireWriteAccess(), h.fileBrowserHandlers.DeleteMultipleFiles)
|
||||
filesApi.POST("/create-folder", dash.RequireWriteAccess(), h.fileBrowserHandlers.CreateFolder)
|
||||
filesApi.POST("/upload", dash.RequireWriteAccess(), h.fileBrowserHandlers.UploadFile)
|
||||
filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile)
|
||||
filesApi.GET("/view", h.fileBrowserHandlers.ViewFile)
|
||||
filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties)
|
||||
}
|
||||
|
||||
// Volume management API routes
|
||||
volumeApi := api.Group("/volumes")
|
||||
{
|
||||
volumeApi.POST("/:id/:server/vacuum", dash.RequireWriteAccess(), h.clusterHandlers.VacuumVolume)
|
||||
}
|
||||
|
||||
// Plugin API routes
|
||||
pluginApi := api.Group("/plugin")
|
||||
{
|
||||
pluginApi.GET("/status", h.adminServer.GetPluginStatusAPI)
|
||||
pluginApi.GET("/workers", h.adminServer.GetPluginWorkersAPI)
|
||||
pluginApi.GET("/job-types", h.adminServer.GetPluginJobTypesAPI)
|
||||
pluginApi.GET("/jobs", h.adminServer.GetPluginJobsAPI)
|
||||
pluginApi.GET("/jobs/:jobId", h.adminServer.GetPluginJobAPI)
|
||||
pluginApi.GET("/jobs/:jobId/detail", h.adminServer.GetPluginJobDetailAPI)
|
||||
pluginApi.GET("/activities", h.adminServer.GetPluginActivitiesAPI)
|
||||
pluginApi.GET("/scheduler-states", h.adminServer.GetPluginSchedulerStatesAPI)
|
||||
pluginApi.GET("/job-types/:jobType/descriptor", h.adminServer.GetPluginJobTypeDescriptorAPI)
|
||||
pluginApi.POST("/job-types/:jobType/schema", h.adminServer.RequestPluginJobTypeSchemaAPI)
|
||||
pluginApi.GET("/job-types/:jobType/config", h.adminServer.GetPluginJobTypeConfigAPI)
|
||||
pluginApi.PUT("/job-types/:jobType/config", dash.RequireWriteAccess(), h.adminServer.UpdatePluginJobTypeConfigAPI)
|
||||
pluginApi.GET("/job-types/:jobType/runs", h.adminServer.GetPluginRunHistoryAPI)
|
||||
pluginApi.POST("/job-types/:jobType/detect", dash.RequireWriteAccess(), h.adminServer.TriggerPluginDetectionAPI)
|
||||
pluginApi.POST("/job-types/:jobType/run", dash.RequireWriteAccess(), h.adminServer.RunPluginJobTypeAPI)
|
||||
pluginApi.POST("/jobs/execute", dash.RequireWriteAccess(), h.adminServer.ExecutePluginJobAPI)
|
||||
}
|
||||
|
||||
// Message Queue API routes
|
||||
mqApi := api.Group("/mq")
|
||||
{
|
||||
mqApi.GET("/topics/:namespace/:topic", h.mqHandlers.GetTopicDetailsAPI)
|
||||
mqApi.POST("/topics/create", dash.RequireWriteAccess(), h.mqHandlers.CreateTopicAPI)
|
||||
mqApi.POST("/topics/retention/update", dash.RequireWriteAccess(), h.mqHandlers.UpdateTopicRetentionAPI)
|
||||
mqApi.POST("/retention/purge", dash.RequireWriteAccess(), h.adminServer.TriggerTopicRetentionPurgeAPI)
|
||||
}
|
||||
}
|
||||
} 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.userHandlers.ShowObjectStoreUsers)
|
||||
r.GET("/object-store/policies", h.policyHandlers.ShowPolicies)
|
||||
r.GET("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts)
|
||||
r.GET("/object-store/s3tables/buckets", h.ShowS3TablesBuckets)
|
||||
r.GET("/object-store/s3tables/buckets/:bucket/namespaces", h.ShowS3TablesNamespaces)
|
||||
r.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables", h.ShowS3TablesTables)
|
||||
r.GET("/object-store/s3tables/buckets/:bucket/namespaces/:namespace/tables/:table", h.ShowS3TablesTableDetails)
|
||||
r.GET("/object-store/iceberg", h.ShowIcebergCatalog)
|
||||
r.GET("/object-store/iceberg/:catalog/namespaces", h.ShowIcebergNamespaces)
|
||||
r.GET("/object-store/iceberg/:catalog/namespaces/:namespace/tables", h.ShowIcebergTables)
|
||||
r.GET("/object-store/iceberg/:catalog/namespaces/:namespace/tables/:table", h.ShowIcebergTableDetails)
|
||||
|
||||
// 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)
|
||||
|
||||
// Storage management routes
|
||||
r.GET("/storage/volumes", h.clusterHandlers.ShowClusterVolumes)
|
||||
r.GET("/storage/volumes/:id/:server", h.clusterHandlers.ShowVolumeDetails)
|
||||
r.GET("/storage/collections", h.clusterHandlers.ShowClusterCollections)
|
||||
r.GET("/storage/collections/:name", h.clusterHandlers.ShowCollectionDetails)
|
||||
r.GET("/storage/ec-shards", h.clusterHandlers.ShowClusterEcShards)
|
||||
r.GET("/storage/ec-volumes/:id", h.clusterHandlers.ShowEcVolumeDetails)
|
||||
|
||||
// Message Queue management routes
|
||||
r.GET("/mq/brokers", h.mqHandlers.ShowBrokers)
|
||||
r.GET("/mq/topics", h.mqHandlers.ShowTopics)
|
||||
r.GET("/mq/topics/:namespace/:topic", h.mqHandlers.ShowTopicDetails)
|
||||
|
||||
r.GET("/plugin", h.pluginHandlers.ShowPlugin)
|
||||
r.GET("/plugin/configuration", h.pluginHandlers.ShowPluginConfiguration)
|
||||
r.GET("/plugin/queue", h.pluginHandlers.ShowPluginQueue)
|
||||
r.GET("/plugin/detection", h.pluginHandlers.ShowPluginDetection)
|
||||
r.GET("/plugin/execution", h.pluginHandlers.ShowPluginExecution)
|
||||
r.GET("/plugin/monitoring", h.pluginHandlers.ShowPluginMonitoring)
|
||||
|
||||
// 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
|
||||
api.GET("/config", h.adminServer.GetConfigInfo) // Configuration information
|
||||
|
||||
// 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)
|
||||
s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota)
|
||||
s3Api.PUT("/buckets/:bucket/owner", h.adminServer.UpdateBucketOwner)
|
||||
}
|
||||
|
||||
// User management API routes
|
||||
usersApi := api.Group("/users")
|
||||
{
|
||||
usersApi.GET("", h.userHandlers.GetUsers)
|
||||
usersApi.POST("", h.userHandlers.CreateUser)
|
||||
usersApi.GET("/:username", h.userHandlers.GetUserDetails)
|
||||
usersApi.PUT("/:username", h.userHandlers.UpdateUser)
|
||||
usersApi.DELETE("/:username", h.userHandlers.DeleteUser)
|
||||
usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey)
|
||||
usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey)
|
||||
usersApi.PUT("/:username/access-keys/:accessKeyId/status", h.userHandlers.UpdateAccessKeyStatus)
|
||||
usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies)
|
||||
usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies)
|
||||
}
|
||||
|
||||
// Service Account management API routes
|
||||
saApi := api.Group("/service-accounts")
|
||||
{
|
||||
saApi.GET("", h.serviceAccountHandlers.GetServiceAccounts)
|
||||
saApi.POST("", h.serviceAccountHandlers.CreateServiceAccount)
|
||||
saApi.GET("/:id", h.serviceAccountHandlers.GetServiceAccountDetails)
|
||||
saApi.PUT("/:id", h.serviceAccountHandlers.UpdateServiceAccount)
|
||||
saApi.DELETE("/:id", h.serviceAccountHandlers.DeleteServiceAccount)
|
||||
}
|
||||
|
||||
// Object Store Policy management API routes
|
||||
objectStorePoliciesApi := api.Group("/object-store/policies")
|
||||
{
|
||||
objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies)
|
||||
objectStorePoliciesApi.POST("", h.policyHandlers.CreatePolicy)
|
||||
objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy)
|
||||
objectStorePoliciesApi.PUT("/:name", h.policyHandlers.UpdatePolicy)
|
||||
objectStorePoliciesApi.DELETE("/:name", h.policyHandlers.DeletePolicy)
|
||||
objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy)
|
||||
}
|
||||
|
||||
// S3 Tables API routes
|
||||
s3TablesApi := api.Group("/s3tables")
|
||||
{
|
||||
s3TablesApi.GET("/buckets", h.adminServer.ListS3TablesBucketsAPI)
|
||||
s3TablesApi.POST("/buckets", h.adminServer.CreateS3TablesBucket)
|
||||
s3TablesApi.DELETE("/buckets", h.adminServer.DeleteS3TablesBucket)
|
||||
s3TablesApi.GET("/namespaces", h.adminServer.ListS3TablesNamespacesAPI)
|
||||
s3TablesApi.POST("/namespaces", h.adminServer.CreateS3TablesNamespace)
|
||||
s3TablesApi.DELETE("/namespaces", h.adminServer.DeleteS3TablesNamespace)
|
||||
s3TablesApi.GET("/tables", h.adminServer.ListS3TablesTablesAPI)
|
||||
s3TablesApi.POST("/tables", h.adminServer.CreateS3TablesTable)
|
||||
s3TablesApi.DELETE("/tables", h.adminServer.DeleteS3TablesTable)
|
||||
s3TablesApi.PUT("/bucket-policy", h.adminServer.PutS3TablesBucketPolicy)
|
||||
s3TablesApi.GET("/bucket-policy", h.adminServer.GetS3TablesBucketPolicy)
|
||||
s3TablesApi.DELETE("/bucket-policy", h.adminServer.DeleteS3TablesBucketPolicy)
|
||||
s3TablesApi.PUT("/table-policy", h.adminServer.PutS3TablesTablePolicy)
|
||||
s3TablesApi.GET("/table-policy", h.adminServer.GetS3TablesTablePolicy)
|
||||
s3TablesApi.DELETE("/table-policy", h.adminServer.DeleteS3TablesTablePolicy)
|
||||
s3TablesApi.PUT("/tags", h.adminServer.TagS3TablesResource)
|
||||
s3TablesApi.GET("/tags", h.adminServer.ListS3TablesTags)
|
||||
s3TablesApi.DELETE("/tags", h.adminServer.UntagS3TablesResource)
|
||||
}
|
||||
|
||||
// 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)
|
||||
filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile)
|
||||
filesApi.GET("/view", h.fileBrowserHandlers.ViewFile)
|
||||
filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties)
|
||||
}
|
||||
|
||||
// Volume management API routes
|
||||
volumeApi := api.Group("/volumes")
|
||||
{
|
||||
volumeApi.POST("/:id/:server/vacuum", h.clusterHandlers.VacuumVolume)
|
||||
}
|
||||
|
||||
// Plugin API routes
|
||||
pluginApi := api.Group("/plugin")
|
||||
{
|
||||
pluginApi.GET("/status", h.adminServer.GetPluginStatusAPI)
|
||||
pluginApi.GET("/workers", h.adminServer.GetPluginWorkersAPI)
|
||||
pluginApi.GET("/job-types", h.adminServer.GetPluginJobTypesAPI)
|
||||
pluginApi.GET("/jobs", h.adminServer.GetPluginJobsAPI)
|
||||
pluginApi.GET("/jobs/:jobId", h.adminServer.GetPluginJobAPI)
|
||||
pluginApi.GET("/jobs/:jobId/detail", h.adminServer.GetPluginJobDetailAPI)
|
||||
pluginApi.GET("/activities", h.adminServer.GetPluginActivitiesAPI)
|
||||
pluginApi.GET("/scheduler-states", h.adminServer.GetPluginSchedulerStatesAPI)
|
||||
pluginApi.GET("/job-types/:jobType/descriptor", h.adminServer.GetPluginJobTypeDescriptorAPI)
|
||||
pluginApi.POST("/job-types/:jobType/schema", h.adminServer.RequestPluginJobTypeSchemaAPI)
|
||||
pluginApi.GET("/job-types/:jobType/config", h.adminServer.GetPluginJobTypeConfigAPI)
|
||||
pluginApi.PUT("/job-types/:jobType/config", h.adminServer.UpdatePluginJobTypeConfigAPI)
|
||||
pluginApi.GET("/job-types/:jobType/runs", h.adminServer.GetPluginRunHistoryAPI)
|
||||
pluginApi.POST("/job-types/:jobType/detect", h.adminServer.TriggerPluginDetectionAPI)
|
||||
pluginApi.POST("/job-types/:jobType/run", h.adminServer.RunPluginJobTypeAPI)
|
||||
pluginApi.POST("/jobs/execute", h.adminServer.ExecutePluginJobAPI)
|
||||
}
|
||||
|
||||
// Message Queue API routes
|
||||
mqApi := api.Group("/mq")
|
||||
{
|
||||
mqApi.GET("/topics/:namespace/:topic", h.mqHandlers.GetTopicDetailsAPI)
|
||||
mqApi.POST("/topics/create", h.mqHandlers.CreateTopicAPI)
|
||||
mqApi.POST("/topics/retention/update", h.mqHandlers.UpdateTopicRetentionAPI)
|
||||
mqApi.POST("/retention/purge", h.adminServer.TriggerTopicRetentionPurgeAPI)
|
||||
}
|
||||
}
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
api.Use(dash.RequireAuthAPI(h.sessionStore))
|
||||
h.registerAPIRoutes(api, true)
|
||||
return
|
||||
}
|
||||
|
||||
// No authentication required - all routes are public
|
||||
h.registerUIRoutes(r)
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
h.registerAPIRoutes(api, false)
|
||||
}
|
||||
|
||||
func (h *AdminHandlers) registerUIRoutes(r *mux.Router) {
|
||||
// Main admin interface routes
|
||||
r.HandleFunc("/", h.ShowDashboard).Methods(http.MethodGet)
|
||||
r.HandleFunc("/admin", h.ShowDashboard).Methods(http.MethodGet)
|
||||
|
||||
// Object Store management routes
|
||||
r.HandleFunc("/object-store/buckets", h.ShowS3Buckets).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/buckets/{bucket}", h.ShowBucketDetails).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/users", h.userHandlers.ShowObjectStoreUsers).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/policies", h.policyHandlers.ShowPolicies).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/s3tables/buckets", h.ShowS3TablesBuckets).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces", h.ShowS3TablesNamespaces).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables", h.ShowS3TablesTables).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables/{table}", h.ShowS3TablesTableDetails).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/iceberg", h.ShowIcebergCatalog).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/iceberg/{catalog}/namespaces", h.ShowIcebergNamespaces).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/iceberg/{catalog}/namespaces/{namespace}/tables", h.ShowIcebergTables).Methods(http.MethodGet)
|
||||
r.HandleFunc("/object-store/iceberg/{catalog}/namespaces/{namespace}/tables/{table}", h.ShowIcebergTableDetails).Methods(http.MethodGet)
|
||||
|
||||
// File browser routes
|
||||
r.HandleFunc("/files", h.fileBrowserHandlers.ShowFileBrowser).Methods(http.MethodGet)
|
||||
|
||||
// Cluster management routes
|
||||
r.HandleFunc("/cluster/masters", h.clusterHandlers.ShowClusterMasters).Methods(http.MethodGet)
|
||||
r.HandleFunc("/cluster/filers", h.clusterHandlers.ShowClusterFilers).Methods(http.MethodGet)
|
||||
r.HandleFunc("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers).Methods(http.MethodGet)
|
||||
|
||||
// Storage management routes
|
||||
r.HandleFunc("/storage/volumes", h.clusterHandlers.ShowClusterVolumes).Methods(http.MethodGet)
|
||||
r.HandleFunc("/storage/volumes/{id}/{server}", h.clusterHandlers.ShowVolumeDetails).Methods(http.MethodGet)
|
||||
r.HandleFunc("/storage/collections", h.clusterHandlers.ShowClusterCollections).Methods(http.MethodGet)
|
||||
r.HandleFunc("/storage/collections/{name}", h.clusterHandlers.ShowCollectionDetails).Methods(http.MethodGet)
|
||||
r.HandleFunc("/storage/ec-shards", h.clusterHandlers.ShowClusterEcShards).Methods(http.MethodGet)
|
||||
r.HandleFunc("/storage/ec-volumes/{id}", h.clusterHandlers.ShowEcVolumeDetails).Methods(http.MethodGet)
|
||||
|
||||
// Message Queue management routes
|
||||
r.HandleFunc("/mq/brokers", h.mqHandlers.ShowBrokers).Methods(http.MethodGet)
|
||||
r.HandleFunc("/mq/topics", h.mqHandlers.ShowTopics).Methods(http.MethodGet)
|
||||
r.HandleFunc("/mq/topics/{namespace}/{topic}", h.mqHandlers.ShowTopicDetails).Methods(http.MethodGet)
|
||||
|
||||
// Plugin pages
|
||||
r.HandleFunc("/plugin", h.pluginHandlers.ShowPlugin).Methods(http.MethodGet)
|
||||
r.HandleFunc("/plugin/configuration", h.pluginHandlers.ShowPluginConfiguration).Methods(http.MethodGet)
|
||||
r.HandleFunc("/plugin/queue", h.pluginHandlers.ShowPluginQueue).Methods(http.MethodGet)
|
||||
r.HandleFunc("/plugin/detection", h.pluginHandlers.ShowPluginDetection).Methods(http.MethodGet)
|
||||
r.HandleFunc("/plugin/execution", h.pluginHandlers.ShowPluginExecution).Methods(http.MethodGet)
|
||||
r.HandleFunc("/plugin/monitoring", h.pluginHandlers.ShowPluginMonitoring).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (h *AdminHandlers) registerAPIRoutes(api *mux.Router, enforceWrite bool) {
|
||||
wrapWrite := func(handler http.HandlerFunc) http.Handler {
|
||||
if !enforceWrite {
|
||||
return handler
|
||||
}
|
||||
return dash.RequireWriteAccess()(handler)
|
||||
}
|
||||
|
||||
api.HandleFunc("/cluster/topology", h.clusterHandlers.GetClusterTopology).Methods(http.MethodGet)
|
||||
api.HandleFunc("/cluster/masters", h.clusterHandlers.GetMasters).Methods(http.MethodGet)
|
||||
api.HandleFunc("/cluster/volumes", h.clusterHandlers.GetVolumeServers).Methods(http.MethodGet)
|
||||
api.HandleFunc("/admin", h.adminServer.ShowAdmin).Methods(http.MethodGet)
|
||||
api.HandleFunc("/config", h.adminServer.GetConfigInfo).Methods(http.MethodGet)
|
||||
|
||||
s3Api := api.PathPrefix("/s3").Subrouter()
|
||||
s3Api.HandleFunc("/buckets", h.adminServer.ListBucketsAPI).Methods(http.MethodGet)
|
||||
s3Api.Handle("/buckets", wrapWrite(h.adminServer.CreateBucket)).Methods(http.MethodPost)
|
||||
s3Api.Handle("/buckets/{bucket}", wrapWrite(h.adminServer.DeleteBucket)).Methods(http.MethodDelete)
|
||||
s3Api.HandleFunc("/buckets/{bucket}", h.adminServer.ShowBucketDetails).Methods(http.MethodGet)
|
||||
s3Api.Handle("/buckets/{bucket}/quota", wrapWrite(h.adminServer.UpdateBucketQuota)).Methods(http.MethodPut)
|
||||
s3Api.Handle("/buckets/{bucket}/owner", wrapWrite(h.adminServer.UpdateBucketOwner)).Methods(http.MethodPut)
|
||||
|
||||
usersApi := api.PathPrefix("/users").Subrouter()
|
||||
usersApi.HandleFunc("", h.userHandlers.GetUsers).Methods(http.MethodGet)
|
||||
usersApi.Handle("", wrapWrite(h.userHandlers.CreateUser)).Methods(http.MethodPost)
|
||||
usersApi.HandleFunc("/{username}", h.userHandlers.GetUserDetails).Methods(http.MethodGet)
|
||||
usersApi.Handle("/{username}", wrapWrite(h.userHandlers.UpdateUser)).Methods(http.MethodPut)
|
||||
usersApi.Handle("/{username}", wrapWrite(h.userHandlers.DeleteUser)).Methods(http.MethodDelete)
|
||||
usersApi.Handle("/{username}/access-keys", wrapWrite(h.userHandlers.CreateAccessKey)).Methods(http.MethodPost)
|
||||
usersApi.Handle("/{username}/access-keys/{accessKeyId}", wrapWrite(h.userHandlers.DeleteAccessKey)).Methods(http.MethodDelete)
|
||||
usersApi.Handle("/{username}/access-keys/{accessKeyId}/status", wrapWrite(h.userHandlers.UpdateAccessKeyStatus)).Methods(http.MethodPut)
|
||||
usersApi.HandleFunc("/{username}/policies", h.userHandlers.GetUserPolicies).Methods(http.MethodGet)
|
||||
usersApi.Handle("/{username}/policies", wrapWrite(h.userHandlers.UpdateUserPolicies)).Methods(http.MethodPut)
|
||||
|
||||
saApi := api.PathPrefix("/service-accounts").Subrouter()
|
||||
saApi.HandleFunc("", h.serviceAccountHandlers.GetServiceAccounts).Methods(http.MethodGet)
|
||||
saApi.Handle("", wrapWrite(h.serviceAccountHandlers.CreateServiceAccount)).Methods(http.MethodPost)
|
||||
saApi.HandleFunc("/{id}", h.serviceAccountHandlers.GetServiceAccountDetails).Methods(http.MethodGet)
|
||||
saApi.Handle("/{id}", wrapWrite(h.serviceAccountHandlers.UpdateServiceAccount)).Methods(http.MethodPut)
|
||||
saApi.Handle("/{id}", wrapWrite(h.serviceAccountHandlers.DeleteServiceAccount)).Methods(http.MethodDelete)
|
||||
|
||||
policyApi := api.PathPrefix("/object-store/policies").Subrouter()
|
||||
policyApi.HandleFunc("", h.policyHandlers.GetPolicies).Methods(http.MethodGet)
|
||||
policyApi.Handle("", wrapWrite(h.policyHandlers.CreatePolicy)).Methods(http.MethodPost)
|
||||
policyApi.HandleFunc("/{name}", h.policyHandlers.GetPolicy).Methods(http.MethodGet)
|
||||
policyApi.Handle("/{name}", wrapWrite(h.policyHandlers.UpdatePolicy)).Methods(http.MethodPut)
|
||||
policyApi.Handle("/{name}", wrapWrite(h.policyHandlers.DeletePolicy)).Methods(http.MethodDelete)
|
||||
policyApi.HandleFunc("/validate", h.policyHandlers.ValidatePolicy).Methods(http.MethodPost)
|
||||
|
||||
s3TablesApi := api.PathPrefix("/s3tables").Subrouter()
|
||||
s3TablesApi.HandleFunc("/buckets", h.adminServer.ListS3TablesBucketsAPI).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/buckets", wrapWrite(h.adminServer.CreateS3TablesBucket)).Methods(http.MethodPost)
|
||||
s3TablesApi.Handle("/buckets", wrapWrite(h.adminServer.DeleteS3TablesBucket)).Methods(http.MethodDelete)
|
||||
s3TablesApi.HandleFunc("/namespaces", h.adminServer.ListS3TablesNamespacesAPI).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/namespaces", wrapWrite(h.adminServer.CreateS3TablesNamespace)).Methods(http.MethodPost)
|
||||
s3TablesApi.Handle("/namespaces", wrapWrite(h.adminServer.DeleteS3TablesNamespace)).Methods(http.MethodDelete)
|
||||
s3TablesApi.HandleFunc("/tables", h.adminServer.ListS3TablesTablesAPI).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/tables", wrapWrite(h.adminServer.CreateS3TablesTable)).Methods(http.MethodPost)
|
||||
s3TablesApi.Handle("/tables", wrapWrite(h.adminServer.DeleteS3TablesTable)).Methods(http.MethodDelete)
|
||||
s3TablesApi.Handle("/bucket-policy", wrapWrite(h.adminServer.PutS3TablesBucketPolicy)).Methods(http.MethodPut)
|
||||
s3TablesApi.HandleFunc("/bucket-policy", h.adminServer.GetS3TablesBucketPolicy).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/bucket-policy", wrapWrite(h.adminServer.DeleteS3TablesBucketPolicy)).Methods(http.MethodDelete)
|
||||
s3TablesApi.Handle("/table-policy", wrapWrite(h.adminServer.PutS3TablesTablePolicy)).Methods(http.MethodPut)
|
||||
s3TablesApi.HandleFunc("/table-policy", h.adminServer.GetS3TablesTablePolicy).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/table-policy", wrapWrite(h.adminServer.DeleteS3TablesTablePolicy)).Methods(http.MethodDelete)
|
||||
s3TablesApi.Handle("/tags", wrapWrite(h.adminServer.TagS3TablesResource)).Methods(http.MethodPut)
|
||||
s3TablesApi.HandleFunc("/tags", h.adminServer.ListS3TablesTags).Methods(http.MethodGet)
|
||||
s3TablesApi.Handle("/tags", wrapWrite(h.adminServer.UntagS3TablesResource)).Methods(http.MethodDelete)
|
||||
|
||||
filesApi := api.PathPrefix("/files").Subrouter()
|
||||
filesApi.Handle("/delete", wrapWrite(h.fileBrowserHandlers.DeleteFile)).Methods(http.MethodDelete)
|
||||
filesApi.Handle("/delete-multiple", wrapWrite(h.fileBrowserHandlers.DeleteMultipleFiles)).Methods(http.MethodDelete)
|
||||
filesApi.Handle("/create-folder", wrapWrite(h.fileBrowserHandlers.CreateFolder)).Methods(http.MethodPost)
|
||||
filesApi.Handle("/upload", wrapWrite(h.fileBrowserHandlers.UploadFile)).Methods(http.MethodPost)
|
||||
filesApi.HandleFunc("/download", h.fileBrowserHandlers.DownloadFile).Methods(http.MethodGet)
|
||||
filesApi.HandleFunc("/view", h.fileBrowserHandlers.ViewFile).Methods(http.MethodGet)
|
||||
filesApi.HandleFunc("/properties", h.fileBrowserHandlers.GetFileProperties).Methods(http.MethodGet)
|
||||
|
||||
volumeApi := api.PathPrefix("/volumes").Subrouter()
|
||||
volumeApi.Handle("/{id}/{server}/vacuum", wrapWrite(h.clusterHandlers.VacuumVolume)).Methods(http.MethodPost)
|
||||
|
||||
pluginApi := api.PathPrefix("/plugin").Subrouter()
|
||||
pluginApi.HandleFunc("/status", h.adminServer.GetPluginStatusAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/workers", h.adminServer.GetPluginWorkersAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/job-types", h.adminServer.GetPluginJobTypesAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/jobs", h.adminServer.GetPluginJobsAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/jobs/{jobId}", h.adminServer.GetPluginJobAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/jobs/{jobId}/detail", h.adminServer.GetPluginJobDetailAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/activities", h.adminServer.GetPluginActivitiesAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/scheduler-states", h.adminServer.GetPluginSchedulerStatesAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/job-types/{jobType}/descriptor", h.adminServer.GetPluginJobTypeDescriptorAPI).Methods(http.MethodGet)
|
||||
pluginApi.HandleFunc("/job-types/{jobType}/schema", h.adminServer.RequestPluginJobTypeSchemaAPI).Methods(http.MethodPost)
|
||||
pluginApi.HandleFunc("/job-types/{jobType}/config", h.adminServer.GetPluginJobTypeConfigAPI).Methods(http.MethodGet)
|
||||
pluginApi.Handle("/job-types/{jobType}/config", wrapWrite(h.adminServer.UpdatePluginJobTypeConfigAPI)).Methods(http.MethodPut)
|
||||
pluginApi.HandleFunc("/job-types/{jobType}/runs", h.adminServer.GetPluginRunHistoryAPI).Methods(http.MethodGet)
|
||||
pluginApi.Handle("/job-types/{jobType}/detect", wrapWrite(h.adminServer.TriggerPluginDetectionAPI)).Methods(http.MethodPost)
|
||||
pluginApi.Handle("/job-types/{jobType}/run", wrapWrite(h.adminServer.RunPluginJobTypeAPI)).Methods(http.MethodPost)
|
||||
pluginApi.Handle("/jobs/execute", wrapWrite(h.adminServer.ExecutePluginJobAPI)).Methods(http.MethodPost)
|
||||
|
||||
mqApi := api.PathPrefix("/mq").Subrouter()
|
||||
mqApi.HandleFunc("/topics/{namespace}/{topic}", h.mqHandlers.GetTopicDetailsAPI).Methods(http.MethodGet)
|
||||
mqApi.Handle("/topics/create", wrapWrite(h.mqHandlers.CreateTopicAPI)).Methods(http.MethodPost)
|
||||
mqApi.Handle("/topics/retention/update", wrapWrite(h.mqHandlers.UpdateTopicRetentionAPI)).Methods(http.MethodPost)
|
||||
mqApi.Handle("/retention/purge", wrapWrite(h.adminServer.TriggerTopicRetentionPurgeAPI)).Methods(http.MethodPost)
|
||||
}
|
||||
|
||||
// HealthCheck returns the health status of the admin interface
|
||||
func (h *AdminHandlers) HealthCheck(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"health": "ok"})
|
||||
func (h *AdminHandlers) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"health": "ok"})
|
||||
}
|
||||
|
||||
// ShowDashboard renders the main admin dashboard
|
||||
func (h *AdminHandlers) ShowDashboard(c *gin.Context) {
|
||||
func (h *AdminHandlers) ShowDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
// Get admin data from the server
|
||||
adminData := h.getAdminData(c)
|
||||
adminData := h.getAdminData(r)
|
||||
username := h.getUsername(r)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("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()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, adminComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3Buckets renders the Object Store buckets management page
|
||||
func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) {
|
||||
func (h *AdminHandlers) ShowS3Buckets(w http.ResponseWriter, r *http.Request) {
|
||||
// Get Object Store buckets data from the server
|
||||
s3Data := h.getS3BucketsData(c)
|
||||
s3Data := h.getS3BucketsData(r)
|
||||
username := h.getUsername(r)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("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()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, s3Component)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3TablesBuckets renders the S3 Tables buckets page
|
||||
func (h *AdminHandlers) ShowS3TablesBuckets(c *gin.Context) {
|
||||
username := h.getUsername(c)
|
||||
func (h *AdminHandlers) ShowS3TablesBuckets(w http.ResponseWriter, r *http.Request) {
|
||||
username := h.getUsername(r)
|
||||
|
||||
data, err := h.adminServer.GetS3TablesBucketsData(c.Request.Context())
|
||||
data, err := h.adminServer.GetS3TablesBucketsData(r.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables buckets: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get S3 Tables buckets: "+err.Error())
|
||||
return
|
||||
}
|
||||
data.Username = username
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
component := app.S3TablesBuckets(data)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3TablesNamespaces renders namespaces for a table bucket
|
||||
func (h *AdminHandlers) ShowS3TablesNamespaces(c *gin.Context) {
|
||||
username := h.getUsername(c)
|
||||
func (h *AdminHandlers) ShowS3TablesNamespaces(w http.ResponseWriter, r *http.Request) {
|
||||
username := h.getUsername(r)
|
||||
|
||||
bucketName := c.Param("bucket")
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
arn, err := buildS3TablesBucketArn(bucketName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.adminServer.GetS3TablesNamespacesData(c.Request.Context(), arn)
|
||||
data, err := h.adminServer.GetS3TablesNamespacesData(r.Context(), arn)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables namespaces: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get S3 Tables namespaces: "+err.Error())
|
||||
return
|
||||
}
|
||||
data.Username = username
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
component := app.S3TablesNamespaces(data)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3TablesTables renders tables for a namespace
|
||||
func (h *AdminHandlers) ShowS3TablesTables(c *gin.Context) {
|
||||
username := h.getUsername(c)
|
||||
func (h *AdminHandlers) ShowS3TablesTables(w http.ResponseWriter, r *http.Request) {
|
||||
username := h.getUsername(r)
|
||||
|
||||
bucketName := c.Param("bucket")
|
||||
namespace := c.Param("namespace")
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
arn, err := buildS3TablesBucketArn(bucketName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.adminServer.GetS3TablesTablesData(c.Request.Context(), arn, namespace)
|
||||
data, err := h.adminServer.GetS3TablesTablesData(r.Context(), arn, namespace)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 Tables tables: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get S3 Tables tables: "+err.Error())
|
||||
return
|
||||
}
|
||||
data.Username = username
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
component := app.S3TablesTables(data)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ShowS3TablesTableDetails renders Iceberg table metadata and snapshot details on the merged S3 Tables path.
|
||||
func (h *AdminHandlers) ShowS3TablesTableDetails(c *gin.Context) {
|
||||
bucketName := c.Param("bucket")
|
||||
namespace := c.Param("namespace")
|
||||
tableName := c.Param("table")
|
||||
func (h *AdminHandlers) ShowS3TablesTableDetails(w http.ResponseWriter, r *http.Request) {
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
tableName := mux.Vars(r)["table"]
|
||||
arn, err := buildS3TablesBucketArn(bucketName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.adminServer.GetIcebergTableDetailsData(c.Request.Context(), bucketName, arn, namespace, tableName)
|
||||
username := h.getUsername(r)
|
||||
data, err := h.adminServer.GetIcebergTableDetailsData(r.Context(), bucketName, arn, namespace, tableName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get table details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get table details: "+err.Error())
|
||||
return
|
||||
}
|
||||
data.Username = h.getUsername(c)
|
||||
data.Username = username
|
||||
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
component := app.IcebergTableDetails(data)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
if err := layoutComponent.Render(c.Request.Context(), c.Writer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,8 +397,8 @@ func buildS3TablesBucketArn(bucketName string) (string, error) {
|
||||
}
|
||||
|
||||
// getUsername returns the username from context, defaulting to "admin" if not set
|
||||
func (h *AdminHandlers) getUsername(c *gin.Context) string {
|
||||
username := c.GetString("username")
|
||||
func (h *AdminHandlers) getUsername(r *http.Request) string {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
@@ -581,45 +406,45 @@ func (h *AdminHandlers) getUsername(c *gin.Context) string {
|
||||
}
|
||||
|
||||
// ShowIcebergCatalog redirects legacy Iceberg catalog URL to the merged S3 Tables buckets page.
|
||||
func (h *AdminHandlers) ShowIcebergCatalog(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/object-store/s3tables/buckets")
|
||||
func (h *AdminHandlers) ShowIcebergCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/object-store/s3tables/buckets", http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// ShowIcebergNamespaces redirects legacy Iceberg namespaces URL to the merged S3 Tables namespaces page.
|
||||
func (h *AdminHandlers) ShowIcebergNamespaces(c *gin.Context) {
|
||||
catalogName := c.Param("catalog")
|
||||
c.Redirect(http.StatusMovedPermanently, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces")
|
||||
func (h *AdminHandlers) ShowIcebergNamespaces(w http.ResponseWriter, r *http.Request) {
|
||||
catalogName := mux.Vars(r)["catalog"]
|
||||
http.Redirect(w, r, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces", http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// ShowIcebergTables redirects legacy Iceberg tables URL to the merged S3 Tables tables page.
|
||||
func (h *AdminHandlers) ShowIcebergTables(c *gin.Context) {
|
||||
catalogName := c.Param("catalog")
|
||||
namespace := c.Param("namespace")
|
||||
c.Redirect(http.StatusMovedPermanently, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces/"+url.PathEscape(namespace)+"/tables")
|
||||
func (h *AdminHandlers) ShowIcebergTables(w http.ResponseWriter, r *http.Request) {
|
||||
catalogName := mux.Vars(r)["catalog"]
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
http.Redirect(w, r, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces/"+url.PathEscape(namespace)+"/tables", http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// ShowIcebergTableDetails redirects legacy Iceberg table details URL to the merged S3 Tables details page.
|
||||
func (h *AdminHandlers) ShowIcebergTableDetails(c *gin.Context) {
|
||||
catalogName := c.Param("catalog")
|
||||
namespace := c.Param("namespace")
|
||||
tableName := c.Param("table")
|
||||
c.Redirect(http.StatusMovedPermanently, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces/"+url.PathEscape(namespace)+"/tables/"+url.PathEscape(tableName))
|
||||
func (h *AdminHandlers) ShowIcebergTableDetails(w http.ResponseWriter, r *http.Request) {
|
||||
catalogName := mux.Vars(r)["catalog"]
|
||||
namespace := mux.Vars(r)["namespace"]
|
||||
tableName := mux.Vars(r)["table"]
|
||||
http.Redirect(w, r, "/object-store/s3tables/buckets/"+url.PathEscape(catalogName)+"/namespaces/"+url.PathEscape(namespace)+"/tables/"+url.PathEscape(tableName), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
// ShowBucketDetails returns detailed information about a specific bucket
|
||||
func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) {
|
||||
bucketName := c.Param("bucket")
|
||||
func (h *AdminHandlers) ShowBucketDetails(w http.ResponseWriter, r *http.Request) {
|
||||
bucketName := mux.Vars(r)["bucket"]
|
||||
details, err := h.adminServer.GetBucketDetails(bucketName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get bucket details: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, details)
|
||||
writeJSON(w, http.StatusOK, details)
|
||||
}
|
||||
|
||||
// getS3BucketsData retrieves Object Store buckets data from the server
|
||||
func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData {
|
||||
username := c.GetString("username")
|
||||
func (h *AdminHandlers) getS3BucketsData(r *http.Request) dash.S3BucketsData {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
@@ -642,8 +467,8 @@ func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData {
|
||||
}
|
||||
|
||||
// getAdminData retrieves admin data from the server (now uses consolidated method)
|
||||
func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData {
|
||||
username := c.GetString("username")
|
||||
func (h *AdminHandlers) getAdminData(r *http.Request) dash.AdminData {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
|
||||
// Use the consolidated GetAdminData method from AdminServer
|
||||
adminData, err := h.adminServer.GetAdminData(username)
|
||||
|
||||
@@ -1,73 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
)
|
||||
|
||||
func TestSetupRoutes_RegistersPluginSchedulerStatesAPI_NoAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router := mux.NewRouter()
|
||||
|
||||
newRouteTestAdminHandlers().SetupRoutes(router, false, "", "", "", "", true)
|
||||
|
||||
if !hasRoute(router, "GET", "/api/plugin/scheduler-states") {
|
||||
if !hasRoute(router, http.MethodGet, "/api/plugin/scheduler-states") {
|
||||
t.Fatalf("expected GET /api/plugin/scheduler-states to be registered in no-auth mode")
|
||||
}
|
||||
if !hasRoute(router, "GET", "/api/plugin/jobs/:jobId/detail") {
|
||||
if !hasRoute(router, http.MethodGet, "/api/plugin/jobs/example/detail") {
|
||||
t.Fatalf("expected GET /api/plugin/jobs/:jobId/detail to be registered in no-auth mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupRoutes_RegistersPluginSchedulerStatesAPI_WithAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router := mux.NewRouter()
|
||||
|
||||
newRouteTestAdminHandlers().SetupRoutes(router, true, "admin", "password", "", "", true)
|
||||
|
||||
if !hasRoute(router, "GET", "/api/plugin/scheduler-states") {
|
||||
if !hasRoute(router, http.MethodGet, "/api/plugin/scheduler-states") {
|
||||
t.Fatalf("expected GET /api/plugin/scheduler-states to be registered in auth mode")
|
||||
}
|
||||
if !hasRoute(router, "GET", "/api/plugin/jobs/:jobId/detail") {
|
||||
if !hasRoute(router, http.MethodGet, "/api/plugin/jobs/example/detail") {
|
||||
t.Fatalf("expected GET /api/plugin/jobs/:jobId/detail to be registered in auth mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupRoutes_RegistersPluginPages_NoAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router := mux.NewRouter()
|
||||
|
||||
newRouteTestAdminHandlers().SetupRoutes(router, false, "", "", "", "", true)
|
||||
|
||||
assertHasRoute(t, router, "GET", "/plugin")
|
||||
assertHasRoute(t, router, "GET", "/plugin/configuration")
|
||||
assertHasRoute(t, router, "GET", "/plugin/queue")
|
||||
assertHasRoute(t, router, "GET", "/plugin/detection")
|
||||
assertHasRoute(t, router, "GET", "/plugin/execution")
|
||||
assertHasRoute(t, router, "GET", "/plugin/monitoring")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/configuration")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/queue")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/detection")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/execution")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/monitoring")
|
||||
}
|
||||
|
||||
func TestSetupRoutes_RegistersPluginPages_WithAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router := mux.NewRouter()
|
||||
|
||||
newRouteTestAdminHandlers().SetupRoutes(router, true, "admin", "password", "", "", true)
|
||||
|
||||
assertHasRoute(t, router, "GET", "/plugin")
|
||||
assertHasRoute(t, router, "GET", "/plugin/configuration")
|
||||
assertHasRoute(t, router, "GET", "/plugin/queue")
|
||||
assertHasRoute(t, router, "GET", "/plugin/detection")
|
||||
assertHasRoute(t, router, "GET", "/plugin/execution")
|
||||
assertHasRoute(t, router, "GET", "/plugin/monitoring")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/configuration")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/queue")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/detection")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/execution")
|
||||
assertHasRoute(t, router, http.MethodGet, "/plugin/monitoring")
|
||||
}
|
||||
|
||||
func newRouteTestAdminHandlers() *AdminHandlers {
|
||||
adminServer := &dash.AdminServer{}
|
||||
store := sessions.NewCookieStore([]byte("test-session-key"))
|
||||
return &AdminHandlers{
|
||||
adminServer: adminServer,
|
||||
authHandlers: &AuthHandlers{adminServer: adminServer},
|
||||
sessionStore: store,
|
||||
authHandlers: &AuthHandlers{adminServer: adminServer, sessionStore: store},
|
||||
clusterHandlers: &ClusterHandlers{adminServer: adminServer},
|
||||
fileBrowserHandlers: &FileBrowserHandlers{adminServer: adminServer},
|
||||
userHandlers: &UserHandlers{adminServer: adminServer},
|
||||
@@ -78,16 +79,13 @@ func newRouteTestAdminHandlers() *AdminHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
func hasRoute(router *gin.Engine, method string, path string) bool {
|
||||
for _, route := range router.Routes() {
|
||||
if route.Method == method && route.Path == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
func hasRoute(router *mux.Router, method string, path string) bool {
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
var match mux.RouteMatch
|
||||
return router.Match(req, &match)
|
||||
}
|
||||
|
||||
func assertHasRoute(t *testing.T, router *gin.Engine, method string, path string) {
|
||||
func assertHasRoute(t *testing.T, router *mux.Router, method string, path string) {
|
||||
t.Helper()
|
||||
if !hasRoute(router, method, path) {
|
||||
t.Fatalf("expected %s %s to be registered", method, path)
|
||||
|
||||
@@ -3,52 +3,65 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
)
|
||||
|
||||
// AuthHandlers contains authentication-related HTTP handlers
|
||||
type AuthHandlers struct {
|
||||
adminServer *dash.AdminServer
|
||||
adminServer *dash.AdminServer
|
||||
sessionStore sessions.Store
|
||||
}
|
||||
|
||||
// NewAuthHandlers creates a new instance of AuthHandlers
|
||||
func NewAuthHandlers(adminServer *dash.AdminServer) *AuthHandlers {
|
||||
func NewAuthHandlers(adminServer *dash.AdminServer, store sessions.Store) *AuthHandlers {
|
||||
return &AuthHandlers{
|
||||
adminServer: adminServer,
|
||||
adminServer: adminServer,
|
||||
sessionStore: store,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowLogin displays the login page
|
||||
func (a *AuthHandlers) ShowLogin(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// If already authenticated, redirect to admin
|
||||
if session.Get("authenticated") == true {
|
||||
c.Redirect(http.StatusSeeOther, "/admin")
|
||||
return
|
||||
func (a *AuthHandlers) ShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := a.sessionStore.Get(r, dash.SessionName())
|
||||
var csrfToken string
|
||||
if err == nil {
|
||||
if authenticated, _ := session.Values["authenticated"].(bool); authenticated {
|
||||
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
glog.V(1).Infof("Failed to load session for login page: %v", err)
|
||||
}
|
||||
|
||||
errorMessage := c.Query("error")
|
||||
if session != nil {
|
||||
token, tokenErr := dash.EnsureSessionCSRFToken(session, r, w)
|
||||
if tokenErr != nil {
|
||||
glog.V(1).Infof("Failed to ensure CSRF token for login page: %v", tokenErr)
|
||||
} else {
|
||||
csrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
errorMessage := r.URL.Query().Get("error")
|
||||
|
||||
// Render login template
|
||||
c.Header("Content-Type", "text/html")
|
||||
loginComponent := layout.LoginForm(c, "SeaweedFS Admin", errorMessage)
|
||||
err := loginComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render login template: " + err.Error()})
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
loginComponent := layout.LoginForm("SeaweedFS Admin", errorMessage, csrfToken)
|
||||
if err := loginComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render login template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HandleLogin handles login form submission
|
||||
func (a *AuthHandlers) HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword string) gin.HandlerFunc {
|
||||
return a.adminServer.HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword)
|
||||
func (a *AuthHandlers) HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword string) http.HandlerFunc {
|
||||
return a.adminServer.HandleLogin(a.sessionStore, adminUser, adminPassword, readOnlyUser, readOnlyPassword)
|
||||
}
|
||||
|
||||
// HandleLogout handles user logout
|
||||
func (a *AuthHandlers) HandleLogout(c *gin.Context) {
|
||||
a.adminServer.HandleLogout(c)
|
||||
func (a *AuthHandlers) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
a.adminServer.HandleLogout(a.sessionStore, w, r)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
@@ -24,402 +24,387 @@ func NewClusterHandlers(adminServer *dash.AdminServer) *ClusterHandlers {
|
||||
}
|
||||
|
||||
// ShowClusterVolumeServers renders the cluster volume servers page
|
||||
func (h *ClusterHandlers) ShowClusterVolumeServers(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterVolumeServers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster volume servers data
|
||||
volumeServersData, err := h.adminServer.GetClusterVolumeServers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster volume servers: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster volume servers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
volumeServersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
volumeServersComponent := app.ClusterVolumeServers(*volumeServersData)
|
||||
layoutComponent := layout.Layout(c, volumeServersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, volumeServersComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterVolumes renders the cluster volumes page
|
||||
func (h *ClusterHandlers) ShowClusterVolumes(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterVolumes(w http.ResponseWriter, r *http.Request) {
|
||||
// Get pagination and sorting parameters from query string
|
||||
page := 1
|
||||
if p := c.Query("page"); p != "" {
|
||||
if p := r.URL.Query().Get("page"); p != "" {
|
||||
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
||||
page = parsed
|
||||
}
|
||||
}
|
||||
|
||||
pageSize := 100
|
||||
if ps := c.Query("pageSize"); ps != "" {
|
||||
if ps := r.URL.Query().Get("pageSize"); ps != "" {
|
||||
if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 && parsed <= 1000 {
|
||||
pageSize = parsed
|
||||
}
|
||||
}
|
||||
|
||||
sortBy := c.DefaultQuery("sortBy", "id")
|
||||
sortOrder := c.DefaultQuery("sortOrder", "asc")
|
||||
collection := c.Query("collection") // Optional collection filter
|
||||
sortBy := defaultQuery(r.URL.Query().Get("sortBy"), "id")
|
||||
sortOrder := defaultQuery(r.URL.Query().Get("sortOrder"), "asc")
|
||||
collection := r.URL.Query().Get("collection") // Optional collection filter
|
||||
|
||||
// Get cluster volumes data
|
||||
volumesData, err := h.adminServer.GetClusterVolumes(page, pageSize, sortBy, sortOrder, collection)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster volumes: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster volumes: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
volumesData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
volumesComponent := app.ClusterVolumes(*volumesData)
|
||||
layoutComponent := layout.Layout(c, volumesComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, volumesComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowVolumeDetails renders the volume details page
|
||||
func (h *ClusterHandlers) ShowVolumeDetails(c *gin.Context) {
|
||||
volumeIDStr := c.Param("id")
|
||||
server := c.Param("server")
|
||||
func (h *ClusterHandlers) ShowVolumeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
volumeIDStr := vars["id"]
|
||||
server := vars["server"]
|
||||
|
||||
if volumeIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Volume ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
if server == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Server is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Server is required")
|
||||
return
|
||||
}
|
||||
|
||||
volumeID, err := strconv.Atoi(volumeIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid volume ID"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid volume ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Get volume details
|
||||
volumeDetails, err := h.adminServer.GetVolumeDetails(volumeID, server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get volume details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get volume details: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
username := usernameOrDefault(r)
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
volumeDetailsComponent := app.VolumeDetails(*volumeDetails)
|
||||
layoutComponent := layout.Layout(c, volumeDetailsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, volumeDetailsComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterCollections renders the cluster collections page
|
||||
func (h *ClusterHandlers) ShowClusterCollections(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterCollections(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster collections data
|
||||
collectionsData, err := h.adminServer.GetClusterCollections()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster collections: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster collections: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
collectionsData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
collectionsComponent := app.ClusterCollections(*collectionsData)
|
||||
layoutComponent := layout.Layout(c, collectionsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, collectionsComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCollectionDetails renders the collection detail page
|
||||
func (h *ClusterHandlers) ShowCollectionDetails(c *gin.Context) {
|
||||
collectionName := c.Param("name")
|
||||
func (h *ClusterHandlers) ShowCollectionDetails(w http.ResponseWriter, r *http.Request) {
|
||||
collectionName := mux.Vars(r)["name"]
|
||||
if collectionName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Collection name is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Collection name is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "25"))
|
||||
sortBy := c.DefaultQuery("sort_by", "volume_id")
|
||||
sortOrder := c.DefaultQuery("sort_order", "asc")
|
||||
query := r.URL.Query()
|
||||
page, _ := strconv.Atoi(defaultQuery(query.Get("page"), "1"))
|
||||
pageSize, _ := strconv.Atoi(defaultQuery(query.Get("page_size"), "25"))
|
||||
sortBy := defaultQuery(query.Get("sort_by"), "volume_id")
|
||||
sortOrder := defaultQuery(query.Get("sort_order"), "asc")
|
||||
|
||||
// Get collection details data (volumes and EC volumes)
|
||||
collectionDetailsData, err := h.adminServer.GetCollectionDetails(collectionName, page, pageSize, sortBy, sortOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get collection details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get collection details: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
collectionDetailsData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
collectionDetailsComponent := app.CollectionDetails(*collectionDetailsData)
|
||||
layoutComponent := layout.Layout(c, collectionDetailsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, collectionDetailsComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterEcShards handles the cluster EC shards page (individual shards view)
|
||||
func (h *ClusterHandlers) ShowClusterEcShards(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterEcShards(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse query parameters
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100"))
|
||||
sortBy := c.DefaultQuery("sort_by", "volume_id")
|
||||
sortOrder := c.DefaultQuery("sort_order", "asc")
|
||||
collection := c.DefaultQuery("collection", "")
|
||||
query := r.URL.Query()
|
||||
page, _ := strconv.Atoi(defaultQuery(query.Get("page"), "1"))
|
||||
pageSize, _ := strconv.Atoi(defaultQuery(query.Get("page_size"), "100"))
|
||||
sortBy := defaultQuery(query.Get("sort_by"), "volume_id")
|
||||
sortOrder := defaultQuery(query.Get("sort_order"), "asc")
|
||||
collection := defaultQuery(query.Get("collection"), "")
|
||||
|
||||
// Get data from admin server
|
||||
data, err := h.adminServer.GetClusterEcVolumes(page, pageSize, sortBy, sortOrder, collection)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
data.Username = username
|
||||
|
||||
// Render template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
ecVolumesComponent := app.ClusterEcVolumes(*data)
|
||||
layoutComponent := layout.Layout(c, ecVolumesComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, ecVolumesComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowEcVolumeDetails renders the EC volume details page
|
||||
func (h *ClusterHandlers) ShowEcVolumeDetails(c *gin.Context) {
|
||||
volumeIDStr := c.Param("id")
|
||||
func (h *ClusterHandlers) ShowEcVolumeDetails(w http.ResponseWriter, r *http.Request) {
|
||||
volumeIDStr := mux.Vars(r)["id"]
|
||||
|
||||
if volumeIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Volume ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
volumeID, err := strconv.Atoi(volumeIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid volume ID"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid volume ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Check that volumeID is within uint32 range
|
||||
if volumeID < 0 || uint64(volumeID) > math.MaxUint32 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID out of range"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Volume ID out of range")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse sorting parameters
|
||||
sortBy := c.DefaultQuery("sort_by", "shard_id")
|
||||
sortOrder := c.DefaultQuery("sort_order", "asc")
|
||||
query := r.URL.Query()
|
||||
sortBy := defaultQuery(query.Get("sort_by"), "shard_id")
|
||||
sortOrder := defaultQuery(query.Get("sort_order"), "asc")
|
||||
|
||||
// Get EC volume details
|
||||
ecVolumeDetails, err := h.adminServer.GetEcVolumeDetails(uint32(volumeID), sortBy, sortOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get EC volume details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get EC volume details: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
ecVolumeDetails.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
ecVolumeDetailsComponent := app.EcVolumeDetails(*ecVolumeDetails)
|
||||
layoutComponent := layout.Layout(c, ecVolumeDetailsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, ecVolumeDetailsComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterMasters renders the cluster masters page
|
||||
func (h *ClusterHandlers) ShowClusterMasters(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterMasters(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster masters data
|
||||
mastersData, err := h.adminServer.GetClusterMasters()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster masters: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster masters: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
mastersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
mastersComponent := app.ClusterMasters(*mastersData)
|
||||
layoutComponent := layout.Layout(c, mastersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, mastersComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterFilers renders the cluster filers page
|
||||
func (h *ClusterHandlers) ShowClusterFilers(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterFilers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster filers data
|
||||
filersData, err := h.adminServer.GetClusterFilers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster filers: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster filers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
filersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
filersComponent := app.ClusterFilers(*filersData)
|
||||
layoutComponent := layout.Layout(c, filersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, filersComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowClusterBrokers renders the cluster message brokers page
|
||||
func (h *ClusterHandlers) ShowClusterBrokers(c *gin.Context) {
|
||||
func (h *ClusterHandlers) ShowClusterBrokers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster brokers data
|
||||
brokersData, err := h.adminServer.GetClusterBrokers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster brokers: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster brokers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
username := usernameOrDefault(r)
|
||||
brokersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
brokersComponent := app.ClusterBrokers(*brokersData)
|
||||
layoutComponent := layout.Layout(c, brokersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, brokersComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetClusterTopology returns the cluster topology as JSON
|
||||
func (h *ClusterHandlers) GetClusterTopology(c *gin.Context) {
|
||||
func (h *ClusterHandlers) GetClusterTopology(w http.ResponseWriter, r *http.Request) {
|
||||
topology, err := h.adminServer.GetClusterTopology()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, topology)
|
||||
writeJSON(w, http.StatusOK, topology)
|
||||
}
|
||||
|
||||
// GetMasters returns master node information
|
||||
func (h *ClusterHandlers) GetMasters(c *gin.Context) {
|
||||
// Simple master info
|
||||
c.JSON(http.StatusOK, gin.H{"masters": []gin.H{{"address": "localhost:9333"}}})
|
||||
func (h *ClusterHandlers) GetMasters(w http.ResponseWriter, r *http.Request) {
|
||||
mastersData, err := h.adminServer.GetClusterMasters()
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster masters: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, mastersData)
|
||||
}
|
||||
|
||||
// GetVolumeServers returns volume server information
|
||||
func (h *ClusterHandlers) GetVolumeServers(c *gin.Context) {
|
||||
func (h *ClusterHandlers) GetVolumeServers(w http.ResponseWriter, r *http.Request) {
|
||||
topology, err := h.adminServer.GetClusterTopology()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"volume_servers": topology.VolumeServers})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"volume_servers": topology.VolumeServers})
|
||||
}
|
||||
|
||||
// VacuumVolume handles volume vacuum requests via API
|
||||
func (h *ClusterHandlers) VacuumVolume(c *gin.Context) {
|
||||
volumeIDStr := c.Param("id")
|
||||
server := c.Param("server")
|
||||
func (h *ClusterHandlers) VacuumVolume(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
volumeIDStr := vars["id"]
|
||||
server := vars["server"]
|
||||
|
||||
if volumeIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Volume ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Volume ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
volumeID, err := strconv.Atoi(volumeIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid volume ID"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid volume ID")
|
||||
return
|
||||
}
|
||||
|
||||
if server == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "Server is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Perform vacuum operation
|
||||
err = h.adminServer.VacuumVolume(volumeID, server)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to vacuum volume: " + err.Error(),
|
||||
})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to vacuum volume: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Volume vacuum started successfully",
|
||||
"volume_id": volumeID,
|
||||
"server": server,
|
||||
})
|
||||
}
|
||||
|
||||
func usernameOrDefault(r *http.Request) string {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
return "admin"
|
||||
}
|
||||
return username
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"strings"
|
||||
"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"
|
||||
@@ -59,16 +58,16 @@ func (h *FileBrowserHandlers) newClientWithTimeout(timeout time.Duration) http.C
|
||||
}
|
||||
|
||||
// ShowFileBrowser renders the file browser page
|
||||
func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) {
|
||||
func (h *FileBrowserHandlers) ShowFileBrowser(w http.ResponseWriter, r *http.Request) {
|
||||
// Get path from query parameter, default to root
|
||||
path := c.DefaultQuery("path", "/")
|
||||
path := defaultQuery(r.URL.Query().Get("path"), "/")
|
||||
// Normalize Windows-style paths for consistency
|
||||
path = util.CleanWindowsPath(path)
|
||||
|
||||
// Get pagination parameters
|
||||
lastFileName := c.DefaultQuery("lastFileName", "")
|
||||
lastFileName := r.URL.Query().Get("lastFileName")
|
||||
|
||||
pageSize, err := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
pageSize, err := strconv.Atoi(defaultQuery(r.URL.Query().Get("limit"), "20"))
|
||||
if err != nil || pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
@@ -79,36 +78,42 @@ func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) {
|
||||
// Get file browser data with cursor-based pagination
|
||||
browserData, err := h.adminServer.GetFileBrowser(path, lastFileName, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file browser data: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get file browser data: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
browserData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
browserComponent := app.FileBrowser(*browserData)
|
||||
layoutComponent := layout.Layout(c, browserComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, browserComponent)
|
||||
err = layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteFile handles file deletion API requests
|
||||
func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) {
|
||||
func (h *FileBrowserHandlers) DeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Path string `json:"path" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &request); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(request.Path) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "path is required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,29 +129,36 @@ func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) {
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to delete file: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "File deleted successfully"})
|
||||
}
|
||||
|
||||
// DeleteMultipleFiles handles multiple file deletion API requests
|
||||
func (h *FileBrowserHandlers) DeleteMultipleFiles(c *gin.Context) {
|
||||
func (h *FileBrowserHandlers) DeleteMultipleFiles(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Paths []string `json:"paths" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &request); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(request.Paths) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No paths provided"})
|
||||
writeJSONError(w, http.StatusBadRequest, "No paths provided")
|
||||
return
|
||||
}
|
||||
|
||||
for _, path := range request.Paths {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "path is required")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var deletedCount int
|
||||
var failedCount int
|
||||
var errors []string
|
||||
@@ -189,37 +201,40 @@ func (h *FileBrowserHandlers) DeleteMultipleFiles(c *gin.Context) {
|
||||
} else {
|
||||
response["message"] = fmt.Sprintf("Deleted %d item(s), failed to delete %d item(s)", deletedCount, failedCount)
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
} else {
|
||||
response["message"] = "Failed to delete all selected items"
|
||||
c.JSON(http.StatusInternalServerError, response)
|
||||
writeJSON(w, http.StatusInternalServerError, response)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFolder handles folder creation requests
|
||||
func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) {
|
||||
func (h *FileBrowserHandlers) CreateFolder(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Path string `json:"path" binding:"required"`
|
||||
FolderName string `json:"folder_name" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &request); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(request.Path) == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "path is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Clean and validate folder name
|
||||
folderName := strings.TrimSpace(request.FolderName)
|
||||
if folderName == "" || strings.Contains(folderName, "/") || strings.Contains(folderName, "\\") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid folder name"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid folder name")
|
||||
return
|
||||
}
|
||||
|
||||
// Create full path for new folder
|
||||
fullPath := filepath.Join(request.Path, folderName)
|
||||
if !strings.HasPrefix(fullPath, "/") {
|
||||
fullPath = "/" + fullPath
|
||||
}
|
||||
base := "/" + strings.TrimPrefix(request.Path, "/")
|
||||
fullPath := path.Join(base, folderName)
|
||||
|
||||
// Create folder via filer
|
||||
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
@@ -241,32 +256,32 @@ func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) {
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create folder: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Folder created successfully"})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Folder created successfully"})
|
||||
}
|
||||
|
||||
// UploadFile handles file upload requests
|
||||
func (h *FileBrowserHandlers) UploadFile(c *gin.Context) {
|
||||
func (h *FileBrowserHandlers) UploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the current path
|
||||
currentPath := c.PostForm("path")
|
||||
currentPath := r.FormValue("path")
|
||||
if currentPath == "" {
|
||||
currentPath = "/"
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
err := c.Request.ParseMultipartForm(1 << 30) // 1GB max memory for large file uploads
|
||||
err := r.ParseMultipartForm(1 << 30) // 1GB max memory for large file uploads
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse multipart form: " + err.Error()})
|
||||
writeJSONError(w, http.StatusBadRequest, "Failed to parse multipart form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Get uploaded files (supports multiple files)
|
||||
files := c.Request.MultipartForm.File["files"]
|
||||
files := r.MultipartForm.File["files"]
|
||||
if len(files) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"})
|
||||
writeJSONError(w, http.StatusBadRequest, "No files uploaded")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -292,16 +307,8 @@ func (h *FileBrowserHandlers) UploadFile(c *gin.Context) {
|
||||
fullPath = "/" + fullPath
|
||||
}
|
||||
|
||||
// Open the file
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Upload file to filer
|
||||
err = h.uploadFileToFiler(fullPath, fileHeader)
|
||||
file.Close()
|
||||
|
||||
if err != nil {
|
||||
failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err))
|
||||
@@ -331,10 +338,10 @@ func (h *FileBrowserHandlers) UploadFile(c *gin.Context) {
|
||||
} else {
|
||||
response["message"] = fmt.Sprintf("Uploaded %d file(s), %d failed", len(uploadResults), len(failedUploads))
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
} else {
|
||||
response["message"] = "All file uploads failed"
|
||||
c.JSON(http.StatusInternalServerError, response)
|
||||
writeJSON(w, http.StatusInternalServerError, response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -561,23 +568,23 @@ func (h *FileBrowserHandlers) fetchFileContent(filePath string, timeout time.Dur
|
||||
|
||||
// DownloadFile handles file download requests by proxying through the Admin UI server
|
||||
// This ensures mTLS works correctly since the Admin UI server has the client certificates
|
||||
func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
filePath := c.Query("path")
|
||||
func (h *FileBrowserHandlers) DownloadFile(w http.ResponseWriter, r *http.Request) {
|
||||
filePath := r.URL.Query().Get("path")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "File path is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Get filer address
|
||||
filerAddress := h.adminServer.GetFilerAddress()
|
||||
if filerAddress == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Filer address not configured"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Filer address not configured")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate filer address to prevent SSRF
|
||||
if err := h.validateFilerAddress(filerAddress); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid filer address configuration"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Invalid filer address configuration")
|
||||
return
|
||||
}
|
||||
filerHttpAddress := pb.ServerAddress(filerAddress).ToHttpAddress()
|
||||
@@ -585,7 +592,7 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
// Validate and sanitize the file path
|
||||
cleanFilePath, err := h.validateAndCleanFilePath(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file path: " + err.Error()})
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid file path: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -593,7 +600,7 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
downloadURL := fmt.Sprintf("%s%s", filerHttpAddress, cleanFilePath)
|
||||
downloadURL, err = h.httpClient.NormalizeHttpScheme(downloadURL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to construct download URL: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to construct download URL: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -602,9 +609,9 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
// Safe: filerAddress validated by validateFilerAddress() to match configured filer
|
||||
// Safe: cleanFilePath validated and cleaned by validateAndCleanFilePath() to prevent path traversal
|
||||
// Use request context so download is cancelled when client disconnects
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), "GET", downloadURL, nil)
|
||||
req, err := http.NewRequestWithContext(r.Context(), "GET", downloadURL, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create request: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create request: "+err.Error())
|
||||
return
|
||||
}
|
||||
client := h.newClientWithTimeout(5 * time.Minute) // Longer timeout for large file downloads
|
||||
@@ -613,7 +620,7 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to fetch file from filer: " + err.Error()})
|
||||
writeJSONError(w, http.StatusBadGateway, "Failed to fetch file from filer: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -621,10 +628,10 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
c.JSON(resp.StatusCode, gin.H{"error": fmt.Sprintf("Filer returned status %d but failed to read response body: %v", resp.StatusCode, err)})
|
||||
writeJSONError(w, resp.StatusCode, fmt.Sprintf("Filer returned status %d but failed to read response body: %v", resp.StatusCode, err))
|
||||
return
|
||||
}
|
||||
c.JSON(resp.StatusCode, gin.H{"error": fmt.Sprintf("Filer returned status %d: %s", resp.StatusCode, string(body))})
|
||||
writeJSONError(w, resp.StatusCode, fmt.Sprintf("Filer returned status %d: %s", resp.StatusCode, string(body)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -632,33 +639,33 @@ func (h *FileBrowserHandlers) DownloadFile(c *gin.Context) {
|
||||
fileName := filepath.Base(cleanFilePath)
|
||||
// Use mime.FormatMediaType for RFC 6266 compliant Content-Disposition,
|
||||
// properly handling non-ASCII characters and special characters
|
||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": fileName}))
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": fileName}))
|
||||
|
||||
// Use content type from filer response, or default to octet-stream
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
c.Header("Content-Type", contentType)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
// Set content length if available
|
||||
if resp.ContentLength > 0 {
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", resp.ContentLength))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", resp.ContentLength))
|
||||
}
|
||||
|
||||
// Stream the response body to the client
|
||||
c.Status(http.StatusOK)
|
||||
_, err = io.Copy(c.Writer, resp.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
glog.Errorf("Error streaming file download: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ViewFile handles file viewing requests (for text files, images, etc.)
|
||||
func (h *FileBrowserHandlers) ViewFile(c *gin.Context) {
|
||||
filePath := c.Query("path")
|
||||
func (h *FileBrowserHandlers) ViewFile(w http.ResponseWriter, r *http.Request) {
|
||||
filePath := r.URL.Query().Get("path")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "File path is required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -704,7 +711,7 @@ func (h *FileBrowserHandlers) ViewFile(c *gin.Context) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file metadata: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get file metadata: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -752,7 +759,7 @@ func (h *FileBrowserHandlers) ViewFile(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"file": fileEntry,
|
||||
"content": content,
|
||||
"viewable": viewable,
|
||||
@@ -761,10 +768,10 @@ func (h *FileBrowserHandlers) ViewFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetFileProperties handles file properties requests
|
||||
func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) {
|
||||
filePath := c.Query("path")
|
||||
func (h *FileBrowserHandlers) GetFileProperties(w http.ResponseWriter, r *http.Request) {
|
||||
filePath := r.URL.Query().Get("path")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File path is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "File path is required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -853,11 +860,11 @@ func (h *FileBrowserHandlers) GetFileProperties(c *gin.Context) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file properties: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get file properties: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, properties)
|
||||
writeJSON(w, http.StatusOK, properties)
|
||||
}
|
||||
|
||||
// Helper function to format bytes
|
||||
|
||||
28
weed/admin/handlers/http_helpers.go
Normal file
28
weed/admin/handlers/http_helpers.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/internal/httputil"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload interface{}) {
|
||||
httputil.WriteJSON(w, status, payload)
|
||||
}
|
||||
|
||||
func writeJSONError(w http.ResponseWriter, status int, message string) {
|
||||
httputil.WriteJSONError(w, status, message)
|
||||
}
|
||||
|
||||
func decodeJSONBody(r io.Reader, v interface{}) error {
|
||||
return httputil.DecodeJSONBody(r, v)
|
||||
}
|
||||
|
||||
func newJSONMaxReader(w http.ResponseWriter, r *http.Request) io.Reader {
|
||||
return httputil.NewJSONMaxReader(w, r)
|
||||
}
|
||||
|
||||
func defaultQuery(value, fallback string) string {
|
||||
return httputil.DefaultQuery(value, fallback)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
@@ -23,146 +23,152 @@ func NewMessageQueueHandlers(adminServer *dash.AdminServer) *MessageQueueHandler
|
||||
}
|
||||
|
||||
// ShowBrokers renders the message queue brokers page
|
||||
func (h *MessageQueueHandlers) ShowBrokers(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) ShowBrokers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get cluster brokers data
|
||||
brokersData, err := h.adminServer.GetClusterBrokers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster brokers: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get cluster brokers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
brokersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
brokersComponent := app.ClusterBrokers(*brokersData)
|
||||
layoutComponent := layout.Layout(c, brokersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, brokersComponent)
|
||||
err = layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowTopics renders the message queue topics page
|
||||
func (h *MessageQueueHandlers) ShowTopics(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) ShowTopics(w http.ResponseWriter, r *http.Request) {
|
||||
// Get topics data
|
||||
topicsData, err := h.adminServer.GetTopics()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get topics: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get topics: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
topicsData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
topicsComponent := app.Topics(*topicsData)
|
||||
layoutComponent := layout.Layout(c, topicsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, topicsComponent)
|
||||
err = layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowSubscribers renders the message queue subscribers page
|
||||
func (h *MessageQueueHandlers) ShowSubscribers(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) ShowSubscribers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get subscribers data
|
||||
subscribersData, err := h.adminServer.GetSubscribers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get subscribers: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get subscribers: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
subscribersData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
subscribersComponent := app.Subscribers(*subscribersData)
|
||||
layoutComponent := layout.Layout(c, subscribersComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, subscribersComponent)
|
||||
err = layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ShowTopicDetails renders the topic details page
|
||||
func (h *MessageQueueHandlers) ShowTopicDetails(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) ShowTopicDetails(w http.ResponseWriter, r *http.Request) {
|
||||
// Get topic parameters from URL
|
||||
namespace := c.Param("namespace")
|
||||
topicName := c.Param("topic")
|
||||
vars := mux.Vars(r)
|
||||
namespace := vars["namespace"]
|
||||
topicName := vars["topic"]
|
||||
|
||||
if namespace == "" || topicName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing namespace or topic name"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Missing namespace or topic name")
|
||||
return
|
||||
}
|
||||
|
||||
// Get topic details data
|
||||
topicDetailsData, err := h.adminServer.GetTopicDetails(namespace, topicName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get topic details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get topic details: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Set username
|
||||
username := c.GetString("username")
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
topicDetailsData.Username = username
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
topicDetailsComponent := app.TopicDetails(*topicDetailsData)
|
||||
layoutComponent := layout.Layout(c, topicDetailsComponent)
|
||||
err = layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, topicDetailsComponent)
|
||||
err = layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetTopicDetailsAPI returns topic details as JSON for AJAX calls
|
||||
func (h *MessageQueueHandlers) GetTopicDetailsAPI(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) GetTopicDetailsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// Get topic parameters from URL
|
||||
namespace := c.Param("namespace")
|
||||
topicName := c.Param("topic")
|
||||
vars := mux.Vars(r)
|
||||
namespace := vars["namespace"]
|
||||
topicName := vars["topic"]
|
||||
|
||||
if namespace == "" || topicName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing namespace or topic name"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Missing namespace or topic name")
|
||||
return
|
||||
}
|
||||
|
||||
// Get topic details data
|
||||
topicDetailsData, err := h.adminServer.GetTopicDetails(namespace, topicName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get topic details: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get topic details: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return JSON data
|
||||
c.JSON(http.StatusOK, topicDetailsData)
|
||||
writeJSON(w, http.StatusOK, topicDetailsData)
|
||||
}
|
||||
|
||||
// CreateTopicAPI creates a new topic with retention configuration
|
||||
func (h *MessageQueueHandlers) CreateTopicAPI(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) CreateTopicAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Namespace string `json:"namespace" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
@@ -173,30 +179,30 @@ func (h *MessageQueueHandlers) CreateTopicAPI(c *gin.Context) {
|
||||
} `json:"retention"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if req.PartitionCount < 1 || req.PartitionCount > 100 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Partition count must be between 1 and 100"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Partition count must be between 1 and 100")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Retention.Enabled && req.Retention.RetentionSeconds <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Retention seconds must be positive when retention is enabled"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Retention seconds must be positive when retention is enabled")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the topic via admin server
|
||||
err := h.adminServer.CreateTopicWithRetention(req.Namespace, req.Name, req.PartitionCount, req.Retention.Enabled, req.Retention.RetentionSeconds)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create topic: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create topic: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Topic created successfully",
|
||||
"topic": fmt.Sprintf("%s.%s", req.Namespace, req.Name),
|
||||
})
|
||||
@@ -211,27 +217,27 @@ type UpdateTopicRetentionRequest struct {
|
||||
} `json:"retention"`
|
||||
}
|
||||
|
||||
func (h *MessageQueueHandlers) UpdateTopicRetentionAPI(c *gin.Context) {
|
||||
func (h *MessageQueueHandlers) UpdateTopicRetentionAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var request UpdateTopicRetentionRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &request); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if request.Namespace == "" || request.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "namespace and name are required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "namespace and name are required")
|
||||
return
|
||||
}
|
||||
|
||||
// Update the topic retention
|
||||
err := h.adminServer.UpdateTopicRetention(request.Namespace, request.Name, request.Retention.Enabled, request.Retention.RetentionSeconds)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Topic retention updated successfully",
|
||||
"topic": request.Namespace + "." + request.Name,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
@@ -23,45 +22,48 @@ func NewPluginHandlers(adminServer *dash.AdminServer) *PluginHandlers {
|
||||
}
|
||||
|
||||
// ShowPlugin displays plugin overview page.
|
||||
func (h *PluginHandlers) ShowPlugin(c *gin.Context) {
|
||||
h.renderPluginPage(c, "overview")
|
||||
func (h *PluginHandlers) ShowPlugin(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPluginPage(w, r, "overview")
|
||||
}
|
||||
|
||||
// ShowPluginConfiguration displays plugin configuration page.
|
||||
func (h *PluginHandlers) ShowPluginConfiguration(c *gin.Context) {
|
||||
h.renderPluginPage(c, "configuration")
|
||||
func (h *PluginHandlers) ShowPluginConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPluginPage(w, r, "configuration")
|
||||
}
|
||||
|
||||
// ShowPluginDetection displays plugin detection jobs page.
|
||||
func (h *PluginHandlers) ShowPluginDetection(c *gin.Context) {
|
||||
h.renderPluginPage(c, "detection")
|
||||
func (h *PluginHandlers) ShowPluginDetection(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPluginPage(w, r, "detection")
|
||||
}
|
||||
|
||||
// ShowPluginQueue displays plugin job queue page.
|
||||
func (h *PluginHandlers) ShowPluginQueue(c *gin.Context) {
|
||||
h.renderPluginPage(c, "queue")
|
||||
func (h *PluginHandlers) ShowPluginQueue(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPluginPage(w, r, "queue")
|
||||
}
|
||||
|
||||
// ShowPluginExecution displays plugin execution jobs page.
|
||||
func (h *PluginHandlers) ShowPluginExecution(c *gin.Context) {
|
||||
h.renderPluginPage(c, "execution")
|
||||
func (h *PluginHandlers) ShowPluginExecution(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderPluginPage(w, r, "execution")
|
||||
}
|
||||
|
||||
// ShowPluginMonitoring displays plugin monitoring page.
|
||||
func (h *PluginHandlers) ShowPluginMonitoring(c *gin.Context) {
|
||||
func (h *PluginHandlers) ShowPluginMonitoring(w http.ResponseWriter, r *http.Request) {
|
||||
// Backward-compatible alias for the old monitoring URL.
|
||||
h.renderPluginPage(c, "detection")
|
||||
h.renderPluginPage(w, r, "detection")
|
||||
}
|
||||
|
||||
func (h *PluginHandlers) renderPluginPage(c *gin.Context, page string) {
|
||||
func (h *PluginHandlers) renderPluginPage(w http.ResponseWriter, r *http.Request, page string) {
|
||||
component := app.Plugin(page)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := layoutComponent.Render(c.Request.Context(), &buf); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
if err := layoutComponent.Render(r.Context(), &buf); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
@@ -26,53 +26,53 @@ func NewPolicyHandlers(adminServer *dash.AdminServer) *PolicyHandlers {
|
||||
}
|
||||
|
||||
// ShowPolicies renders the policies management page
|
||||
func (h *PolicyHandlers) ShowPolicies(c *gin.Context) {
|
||||
func (h *PolicyHandlers) ShowPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
// Get policies data from the server
|
||||
policiesData := h.getPoliciesData(c)
|
||||
policiesData := h.getPoliciesData(r)
|
||||
|
||||
// Render HTML template
|
||||
c.Header("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
policiesComponent := app.Policies(policiesData)
|
||||
layoutComponent := layout.Layout(c, policiesComponent)
|
||||
err := layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, policiesComponent)
|
||||
if err := layoutComponent.Render(r.Context(), w); err != nil {
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetPolicies returns the list of policies as JSON
|
||||
func (h *PolicyHandlers) GetPolicies(c *gin.Context) {
|
||||
func (h *PolicyHandlers) GetPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
policies, err := h.adminServer.GetPolicies()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policies: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get policies: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"policies": policies})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"policies": policies})
|
||||
}
|
||||
|
||||
// CreatePolicy handles policy creation
|
||||
func (h *PolicyHandlers) CreatePolicy(c *gin.Context) {
|
||||
func (h *PolicyHandlers) CreatePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var req dash.CreatePolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate policy name
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if policy already exists
|
||||
existingPolicy, err := h.adminServer.GetPolicy(req.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
if existingPolicy != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Policy with this name already exists"})
|
||||
writeJSONError(w, http.StatusConflict, "Policy with this name already exists")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,11 +80,11 @@ func (h *PolicyHandlers) CreatePolicy(c *gin.Context) {
|
||||
err = h.adminServer.CreatePolicy(req.Name, req.Document)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create policy %s: %v", req.Name, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Policy created successfully",
|
||||
"policy": req.Name,
|
||||
@@ -92,49 +92,49 @@ func (h *PolicyHandlers) CreatePolicy(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetPolicy returns a specific policy
|
||||
func (h *PolicyHandlers) GetPolicy(c *gin.Context) {
|
||||
policyName := c.Param("name")
|
||||
func (h *PolicyHandlers) GetPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
policyName := mux.Vars(r)["name"]
|
||||
if policyName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
|
||||
return
|
||||
}
|
||||
|
||||
policy, err := h.adminServer.GetPolicy(policyName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if policy == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
|
||||
writeJSONError(w, http.StatusNotFound, "Policy not found")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, policy)
|
||||
writeJSON(w, http.StatusOK, policy)
|
||||
}
|
||||
|
||||
// UpdatePolicy handles policy updates
|
||||
func (h *PolicyHandlers) UpdatePolicy(c *gin.Context) {
|
||||
policyName := c.Param("name")
|
||||
func (h *PolicyHandlers) UpdatePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
policyName := mux.Vars(r)["name"]
|
||||
if policyName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdatePolicyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Check if policy exists
|
||||
existingPolicy, err := h.adminServer.GetPolicy(policyName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
if existingPolicy == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
|
||||
writeJSONError(w, http.StatusNotFound, "Policy not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,11 +142,11 @@ func (h *PolicyHandlers) UpdatePolicy(c *gin.Context) {
|
||||
err = h.adminServer.UpdatePolicy(policyName, req.Document)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to update policy %s: %v", policyName, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to update policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Policy updated successfully",
|
||||
"policy": policyName,
|
||||
@@ -154,21 +154,21 @@ func (h *PolicyHandlers) UpdatePolicy(c *gin.Context) {
|
||||
}
|
||||
|
||||
// DeletePolicy handles policy deletion
|
||||
func (h *PolicyHandlers) DeletePolicy(c *gin.Context) {
|
||||
policyName := c.Param("name")
|
||||
func (h *PolicyHandlers) DeletePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
policyName := mux.Vars(r)["name"]
|
||||
if policyName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy name is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if policy exists
|
||||
existingPolicy, err := h.adminServer.GetPolicy(policyName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
if existingPolicy == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Policy not found"})
|
||||
writeJSONError(w, http.StatusNotFound, "Policy not found")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -176,11 +176,11 @@ func (h *PolicyHandlers) DeletePolicy(c *gin.Context) {
|
||||
err = h.adminServer.DeletePolicy(policyName)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to delete policy %s: %v", policyName, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete policy: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to delete policy: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Policy deleted successfully",
|
||||
"policy": policyName,
|
||||
@@ -188,60 +188,54 @@ func (h *PolicyHandlers) DeletePolicy(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ValidatePolicy validates a policy document without saving it
|
||||
func (h *PolicyHandlers) ValidatePolicy(c *gin.Context) {
|
||||
func (h *PolicyHandlers) ValidatePolicy(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Document policy_engine.PolicyDocument `json:"document" binding:"required"`
|
||||
Document policy_engine.PolicyDocument `json:"document"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if req.Document.Version == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy version is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy version is required")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Document.Statement) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Policy must have at least one statement"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Policy must have at least one statement")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate each statement
|
||||
for i, statement := range req.Document.Statement {
|
||||
if statement.Effect != "Allow" && statement.Effect != "Deny" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i+1),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i+1))
|
||||
return
|
||||
}
|
||||
|
||||
if len(statement.Action.Strings()) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Statement %d: Action is required", i+1),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Action is required", i+1))
|
||||
return
|
||||
}
|
||||
|
||||
if len(statement.Resource.Strings()) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("Statement %d: Resource is required", i+1),
|
||||
})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Resource is required", i+1))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"valid": true,
|
||||
"message": "Policy document is valid",
|
||||
})
|
||||
}
|
||||
|
||||
// getPoliciesData retrieves policies data from the server
|
||||
func (h *PolicyHandlers) getPoliciesData(c *gin.Context) dash.PoliciesData {
|
||||
username := c.GetString("username")
|
||||
func (h *PolicyHandlers) getPoliciesData(r *http.Request) dash.PoliciesData {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
@@ -26,153 +26,154 @@ func NewServiceAccountHandlers(adminServer *dash.AdminServer) *ServiceAccountHan
|
||||
}
|
||||
|
||||
// ShowServiceAccounts renders the service accounts management page
|
||||
func (h *ServiceAccountHandlers) ShowServiceAccounts(c *gin.Context) {
|
||||
data := h.getServiceAccountsData(c)
|
||||
func (h *ServiceAccountHandlers) ShowServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
data := h.getServiceAccountsData(r)
|
||||
|
||||
// Render to buffer first to avoid partial writes on error
|
||||
var buf bytes.Buffer
|
||||
component := app.ServiceAccounts(data)
|
||||
layoutComponent := layout.Layout(c, component)
|
||||
err := layoutComponent.Render(c.Request.Context(), &buf)
|
||||
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, component)
|
||||
err := layoutComponent.Render(r.Context(), &buf)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to render service accounts template: %v", err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Only write to response if render succeeded
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.Writer.Write(buf.Bytes())
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// GetServiceAccounts returns the list of service accounts as JSON
|
||||
func (h *ServiceAccountHandlers) GetServiceAccounts(c *gin.Context) {
|
||||
parentUser := c.Query("parent_user")
|
||||
func (h *ServiceAccountHandlers) GetServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
parentUser := r.URL.Query().Get("parent_user")
|
||||
|
||||
accounts, err := h.adminServer.GetServiceAccounts(c.Request.Context(), parentUser)
|
||||
accounts, err := h.adminServer.GetServiceAccounts(r.Context(), parentUser)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get service accounts: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get service accounts"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get service accounts")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"service_accounts": accounts})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"service_accounts": accounts})
|
||||
}
|
||||
|
||||
// CreateServiceAccount handles service account creation
|
||||
func (h *ServiceAccountHandlers) CreateServiceAccount(c *gin.Context) {
|
||||
func (h *ServiceAccountHandlers) CreateServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
var req dash.CreateServiceAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.ParentUser == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ParentUser is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "ParentUser is required")
|
||||
return
|
||||
}
|
||||
|
||||
sa, err := h.adminServer.CreateServiceAccount(c.Request.Context(), req)
|
||||
sa, err := h.adminServer.CreateServiceAccount(r.Context(), req)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create service account for user %s: %v", req.ParentUser, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service account"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create service account")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Service account created successfully",
|
||||
"service_account": sa,
|
||||
})
|
||||
}
|
||||
|
||||
// GetServiceAccountDetails returns detailed information about a service account
|
||||
func (h *ServiceAccountHandlers) GetServiceAccountDetails(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
func (h *ServiceAccountHandlers) GetServiceAccountDetails(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Service account ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Service account ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
sa, err := h.adminServer.GetServiceAccountDetails(c.Request.Context(), id)
|
||||
sa, err := h.adminServer.GetServiceAccountDetails(r.Context(), id)
|
||||
if err != nil {
|
||||
// Distinguish not-found errors from internal errors
|
||||
if errors.Is(err, dash.ErrServiceAccountNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service account not found: " + err.Error()})
|
||||
writeJSONError(w, http.StatusNotFound, "Service account not found: "+err.Error())
|
||||
} else {
|
||||
glog.Errorf("Failed to get service account details for %s: %v", id, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get service account details"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get service account details")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, sa)
|
||||
writeJSON(w, http.StatusOK, sa)
|
||||
}
|
||||
|
||||
// UpdateServiceAccount handles service account updates
|
||||
func (h *ServiceAccountHandlers) UpdateServiceAccount(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
func (h *ServiceAccountHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Service account ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Service account ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdateServiceAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sa, err := h.adminServer.UpdateServiceAccount(c.Request.Context(), id, req)
|
||||
sa, err := h.adminServer.UpdateServiceAccount(r.Context(), id, req)
|
||||
if err != nil {
|
||||
// Distinguish not-found errors from internal errors
|
||||
if errors.Is(err, dash.ErrServiceAccountNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service account not found"})
|
||||
writeJSONError(w, http.StatusNotFound, "Service account not found")
|
||||
} else {
|
||||
glog.Errorf("Failed to update service account %s: %v", id, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service account"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to update service account")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Service account updated successfully",
|
||||
"service_account": sa,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteServiceAccount handles service account deletion
|
||||
func (h *ServiceAccountHandlers) DeleteServiceAccount(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
func (h *ServiceAccountHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Service account ID is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Service account ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.DeleteServiceAccount(c.Request.Context(), id)
|
||||
err := h.adminServer.DeleteServiceAccount(r.Context(), id)
|
||||
if err != nil {
|
||||
// Distinguish not-found errors from internal errors
|
||||
if errors.Is(err, dash.ErrServiceAccountNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service account not found"})
|
||||
writeJSONError(w, http.StatusNotFound, "Service account not found")
|
||||
} else {
|
||||
glog.Errorf("Failed to delete service account %s: %v", id, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete service account"})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to delete service account")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Service account deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// getServiceAccountsData retrieves service accounts data for the template
|
||||
func (h *ServiceAccountHandlers) getServiceAccountsData(c *gin.Context) dash.ServiceAccountsData {
|
||||
username := c.GetString("username")
|
||||
func (h *ServiceAccountHandlers) getServiceAccountsData(r *http.Request) dash.ServiceAccountsData {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
// Get all service accounts
|
||||
accounts, err := h.adminServer.GetServiceAccounts(c.Request.Context(), "")
|
||||
accounts, err := h.adminServer.GetServiceAccounts(r.Context(), "")
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get service accounts: %v", err)
|
||||
return dash.ServiceAccountsData{
|
||||
@@ -193,7 +194,7 @@ func (h *ServiceAccountHandlers) getServiceAccountsData(c *gin.Context) dash.Ser
|
||||
|
||||
// Get available users for dropdown
|
||||
var availableUsers []string
|
||||
users, err := h.adminServer.GetObjectStoreUsers(c.Request.Context())
|
||||
users, err := h.adminServer.GetObjectStoreUsers(r.Context())
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get users for dropdown: %v", err)
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
|
||||
@@ -25,256 +25,259 @@ func NewUserHandlers(adminServer *dash.AdminServer) *UserHandlers {
|
||||
}
|
||||
|
||||
// ShowObjectStoreUsers renders the object store users management page
|
||||
func (h *UserHandlers) ShowObjectStoreUsers(c *gin.Context) {
|
||||
func (h *UserHandlers) ShowObjectStoreUsers(w http.ResponseWriter, r *http.Request) {
|
||||
// Get object store users data from the server
|
||||
usersData := h.getObjectStoreUsersData(c)
|
||||
usersData := h.getObjectStoreUsersData(r)
|
||||
|
||||
// Render HTML template
|
||||
// Add cache-control headers to prevent browser caching of inline JavaScript
|
||||
c.Header("Content-Type", "text/html")
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
c.Header("ETag", fmt.Sprintf("\"%d\"", time.Now().Unix()))
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.Header().Set("ETag", fmt.Sprintf("\"%d\"", time.Now().Unix()))
|
||||
usersComponent := app.ObjectStoreUsers(usersData)
|
||||
layoutComponent := layout.Layout(c, usersComponent)
|
||||
err := layoutComponent.Render(c.Request.Context(), c.Writer)
|
||||
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
|
||||
layoutComponent := layout.Layout(viewCtx, usersComponent)
|
||||
err := layoutComponent.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetUsers returns the list of users as JSON
|
||||
func (h *UserHandlers) GetUsers(c *gin.Context) {
|
||||
users, err := h.adminServer.GetObjectStoreUsers(c.Request.Context())
|
||||
func (h *UserHandlers) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.adminServer.GetObjectStoreUsers(r.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get users: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"users": users})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"users": users})
|
||||
}
|
||||
|
||||
// CreateUser handles user creation
|
||||
func (h *UserHandlers) CreateUser(c *gin.Context) {
|
||||
func (h *UserHandlers) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req dash.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.adminServer.CreateObjectStoreUser(req)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create user %s: %v", req.Username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "User created successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUser handles user updates
|
||||
func (h *UserHandlers) UpdateUser(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.adminServer.UpdateObjectStoreUser(username, req)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to update user %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to update user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "User updated successfully",
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteUser handles user deletion
|
||||
func (h *UserHandlers) DeleteUser(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.DeleteObjectStoreUser(username)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to delete user %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to delete user: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "User deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserDetails returns detailed information about a specific user
|
||||
func (h *UserHandlers) GetUserDetails(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) GetUserDetails(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.adminServer.GetObjectStoreUserDetails(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found: " + err.Error()})
|
||||
writeJSONError(w, http.StatusNotFound, "User not found: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
writeJSON(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
// CreateAccessKey creates a new access key for a user
|
||||
func (h *UserHandlers) CreateAccessKey(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) CreateAccessKey(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
accessKey, err := h.adminServer.CreateAccessKey(username)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create access key for user %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create access key: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to create access key: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Access key created successfully",
|
||||
"access_key": accessKey,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAccessKey deletes an access key for a user
|
||||
func (h *UserHandlers) DeleteAccessKey(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
accessKeyId := c.Param("accessKeyId")
|
||||
func (h *UserHandlers) DeleteAccessKey(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
accessKeyId := vars["accessKeyId"]
|
||||
|
||||
if username == "" || accessKeyId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username and access key ID are required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username and access key ID are required")
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.DeleteAccessKey(username, accessKeyId)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to delete access key %s for user %s: %v", accessKeyId, username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete access key: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to delete access key: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Access key deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAccessKeyStatus updates the status of an access key for a user
|
||||
func (h *UserHandlers) UpdateAccessKeyStatus(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
accessKeyId := c.Param("accessKeyId")
|
||||
func (h *UserHandlers) UpdateAccessKeyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
accessKeyId := vars["accessKeyId"]
|
||||
|
||||
if username == "" || accessKeyId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username and access key ID are required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username and access key ID are required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdateAccessKeyStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if req.Status != dash.AccessKeyStatusActive && req.Status != dash.AccessKeyStatusInactive {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Status must be '%s' or '%s'", dash.AccessKeyStatusActive, dash.AccessKeyStatusInactive)})
|
||||
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Status must be '%s' or '%s'", dash.AccessKeyStatusActive, dash.AccessKeyStatusInactive))
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.UpdateAccessKeyStatus(username, accessKeyId, req.Status)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to update access key status %s for user %s: %v", accessKeyId, username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update access key status: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to update access key status: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Access key updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserPolicies returns the policies for a user
|
||||
func (h *UserHandlers) GetUserPolicies(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) GetUserPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
policies, err := h.adminServer.GetUserPolicies(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user policies: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to get user policies: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"policies": policies})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"policies": policies})
|
||||
}
|
||||
|
||||
// UpdateUserPolicies updates the policies for a user
|
||||
func (h *UserHandlers) UpdateUserPolicies(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
func (h *UserHandlers) UpdateUserPolicies(w http.ResponseWriter, r *http.Request) {
|
||||
username := mux.Vars(r)["username"]
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
|
||||
writeJSONError(w, http.StatusBadRequest, "Username is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dash.UpdateUserPoliciesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()})
|
||||
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
||||
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.UpdateUserPolicies(username, req.Actions)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to update policies for user %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user policies: " + err.Error()})
|
||||
writeJSONError(w, http.StatusInternalServerError, "Failed to update user policies: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "User policies updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// getObjectStoreUsersData retrieves object store users data from the server
|
||||
func (h *UserHandlers) getObjectStoreUsersData(c *gin.Context) dash.ObjectStoreUsersData {
|
||||
username := c.GetString("username")
|
||||
func (h *UserHandlers) getObjectStoreUsersData(r *http.Request) dash.ObjectStoreUsersData {
|
||||
username := dash.UsernameFromContext(r.Context())
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
|
||||
// Get object store users
|
||||
users, err := h.adminServer.GetObjectStoreUsers(c.Request.Context())
|
||||
users, err := h.adminServer.GetObjectStoreUsers(r.Context())
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to get object store users: %v", err)
|
||||
// Return empty data on error
|
||||
|
||||
Reference in New Issue
Block a user