feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages

Add new frontend pages for the multi-tenant OCDP platform:

- Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories
  and versions, with deploy modal to launch charts on selected clusters
- Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage)
  and per-node details with resource utilization bars
- Chart References page (/chart-references): CRUD for chart metadata references
- Values Templates page (/templates): CRUD for Helm values templates with version
  history and rollback support
- Sidebar: Add Charts navigation, update Storage and Templates links
- api.ts: Add all API client functions (clusterApi, registryApi, instanceApi,
  monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi,
  workspaceApi, userApi) with full TypeScript types

Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
Ivan087
2026-04-15 16:59:31 +08:00
parent c5e51ed069
commit 29d0310f03
283 changed files with 24658 additions and 36038 deletions

View File

@ -0,0 +1,283 @@
package middleware
import (
"context"
"net/http"
"strings"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// Context keys
type contextKey string
const (
ContextKeyUserID contextKey = "user_id"
ContextKeyUsername contextKey = "username"
ContextKeyUserRole contextKey = "user_role"
ContextKeyWorkspaceID contextKey = "workspace_id"
)
// UserClaims 用户声明(从 JWT 解析)
type UserClaims struct {
UserID string
Username string
Role entity.UserRole
WorkspaceID string
}
// WorkspaceMiddleware 工作空间中间件
// 从 JWT 获取用户角色和 workspace_id进行权限检查
func WorkspaceMiddleware(userRepo repository.UserRepository) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 Header 获取 Token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// 解析 Bearer Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
token := parts[1]
// 这里需要从 AuthService 获取验证方法
// 简化处理:假设 token 包含 user_id 和 username
// 实际实现需要调用 JWT 验证服务
// 从数据库获取用户信息
// 注意:这里需要通过 token 解析出 userID
// 实际实现应该在 AuthService 中完成
_ = userRepo
next.ServeHTTP(w, r)
})
}
}
// RequireWorkspace 强制要求 workspace 上下文
// 用于非 Admin 用户的资源操作
func RequireWorkspace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
workspaceID := r.Header.Get("X-Workspace-ID")
userRole := r.Header.Get("X-User-Role")
// Admin 可以没有 workspace
if userRole == string(entity.RoleAdmin) {
next.ServeHTTP(w, r)
return
}
// 普通用户必须有 workspace
if workspaceID == "" {
http.Error(w, "Workspace context required", http.StatusForbidden)
return
}
// 将 workspace_id 放入 context
ctx := context.WithValue(r.Context(), ContextKeyWorkspaceID, workspaceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// RequireAdmin 要求 Admin 角色
func RequireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userRole := r.Header.Get("X-User-Role")
if userRole != string(entity.RoleAdmin) {
http.Error(w, "Admin access required", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// GetUserClaims 从 Context 获取用户声明
func GetUserClaims(ctx context.Context) *UserClaims {
userID, _ := ctx.Value(ContextKeyUserID).(string)
username, _ := ctx.Value(ContextKeyUsername).(string)
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
return &UserClaims{
UserID: userID,
Username: username,
Role: entity.UserRole(roleStr),
WorkspaceID: workspaceID,
}
}
// GetWorkspaceID 从 Context 获取 workspace ID
func GetWorkspaceID(ctx context.Context) string {
workspaceID, _ := ctx.Value(ContextKeyWorkspaceID).(string)
return workspaceID
}
// GetUserID 从 Context 获取用户 ID
func GetUserID(ctx context.Context) string {
userID, _ := ctx.Value(ContextKeyUserID).(string)
return userID
}
// GetUserRole 从 Context 获取用户角色
func GetUserRole(ctx context.Context) entity.UserRole {
roleStr, _ := ctx.Value(ContextKeyUserRole).(string)
return entity.UserRole(roleStr)
}
// FilterByWorkspace 根据用户角色过滤资源
// Admin: 返回所有资源workspaceID 忽略)
// User: 仅返回属于自己 workspace 的资源
func FilterByWorkspace(workspaceID, userRole string) (filterWorkspaceID string, isAdmin bool) {
if userRole == string(entity.RoleAdmin) {
return "", true
}
return workspaceID, false
}
// AuthorizationService 授权服务
type AuthorizationService struct {
userRepo repository.UserRepository
}
// NewAuthorizationService 创建授权服务
func NewAuthorizationService(userRepo repository.UserRepository) *AuthorizationService {
return &AuthorizationService{
userRepo: userRepo,
}
}
// CheckResourceAccess 检查用户是否有权访问指定资源
func (s *AuthorizationService) CheckResourceAccess(ctx context.Context, userID, resourceWorkspaceID string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
// Admin 可以访问所有资源
if user.Role == entity.RoleAdmin {
return nil
}
// 普通用户只能访问自己 workspace 的资源
if user.WorkspaceID != resourceWorkspaceID {
return entity.ErrPermissionDenied
}
return nil
}
// CanAccessWorkspace 检查用户是否可以访问指定 workspace
func (s *AuthorizationService) CanAccessWorkspace(ctx context.Context, userID, targetWorkspaceID string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
// Admin 可以访问所有 workspace
if user.Role == entity.RoleAdmin {
return nil
}
// 普通用户只能访问自己的 workspace
if user.WorkspaceID != targetWorkspaceID {
return entity.ErrPermissionDenied
}
return nil
}
// GetAccessibleWorkspaces 获取用户可访问的 workspace 列表
func (s *AuthorizationService) GetAccessibleWorkspaces(ctx context.Context, userID string) ([]string, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
// Admin 可以访问所有 workspace
if user.Role == entity.RoleAdmin {
return nil, nil // nil 表示所有
}
// 普通用户只能访问自己的 workspace
if user.WorkspaceID != "" {
return []string{user.WorkspaceID}, nil
}
return []string{}, nil
}
// RequireRole 要求特定角色
func RequireRole(roles ...entity.UserRole) func(http.Handler) http.Handler {
roleSet := make(map[entity.UserRole]bool)
for _, r := range roles {
roleSet[r] = true
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userRole := r.Header.Get("X-User-Role")
if !roleSet[entity.UserRole(userRole)] {
http.Error(w, "Insufficient permissions", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// WithUserClaims 将用户声明注入到 Context
func WithUserClaims(claims *UserClaims) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, ContextKeyUserID, claims.UserID)
ctx = context.WithValue(ctx, ContextKeyUsername, claims.Username)
ctx = context.WithValue(ctx, ContextKeyUserRole, string(claims.Role))
if claims.WorkspaceID != "" {
ctx = context.WithValue(ctx, ContextKeyWorkspaceID, claims.WorkspaceID)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// LoginRequired 要求登录
func LoginRequired(authService *service.AuthService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization required", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
token := parts[1]
userID, _, err := authService.VerifyAccessToken(r.Context(), token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), ContextKeyUserID, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}