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:
298
backend/internal/domain/service/user_management_service.go
Normal file
298
backend/internal/domain/service/user_management_service.go
Normal file
@ -0,0 +1,298 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// UserManagementService 用户管理领域服务(仅 Admin 可用)
|
||||
type UserManagementService struct {
|
||||
userRepo repository.UserRepository
|
||||
workspaceRepo repository.WorkspaceRepository
|
||||
passwordHasher PasswordHasher
|
||||
}
|
||||
|
||||
// NewUserManagementService 创建用户管理服务
|
||||
func NewUserManagementService(
|
||||
userRepo repository.UserRepository,
|
||||
workspaceRepo repository.WorkspaceRepository,
|
||||
passwordHasher PasswordHasher,
|
||||
) *UserManagementService {
|
||||
return &UserManagementService{
|
||||
userRepo: userRepo,
|
||||
workspaceRepo: workspaceRepo,
|
||||
passwordHasher: passwordHasher,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser 创建用户(Admin 操作)
|
||||
func (s *UserManagementService) CreateUser(ctx context.Context, username, password, email, role string, workspaceID string) (*entity.User, error) {
|
||||
// 检查用户是否已存在
|
||||
existing, _ := s.userRepo.GetByUsername(ctx, username)
|
||||
if existing != nil {
|
||||
return nil, entity.ErrUserExists
|
||||
}
|
||||
|
||||
// 验证角色
|
||||
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
|
||||
return nil, fmt.Errorf("invalid role: %s", role)
|
||||
}
|
||||
|
||||
// 如果指定了 workspace,验证 workspace 存在
|
||||
if workspaceID != "" {
|
||||
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
|
||||
if err != nil {
|
||||
if err == entity.ErrWorkspaceNotFound {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Admin 不能分配到 workspace
|
||||
if role == string(entity.RoleAdmin) && workspaceID != "" {
|
||||
workspaceID = ""
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
passwordHash, err := s.passwordHasher.Hash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成占位邮箱
|
||||
if email == "" {
|
||||
email = username + "@local.ocdp"
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := entity.NewUser(username, passwordHash, email)
|
||||
user.ID = uuid.New().String()
|
||||
user.Role = entity.UserRole(role)
|
||||
user.WorkspaceID = workspaceID
|
||||
user.IsActive = true
|
||||
user.MustChangePassword = true // 首次登录必须修改密码
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUser 获取用户
|
||||
func (s *UserManagementService) GetUser(ctx context.Context, id string) (*entity.User, error) {
|
||||
return s.userRepo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// ListUsers 列出用户(可筛选 workspace)
|
||||
func (s *UserManagementService) ListUsers(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
if workspaceID != "" {
|
||||
return s.userRepo.ListByWorkspace(ctx, workspaceID)
|
||||
}
|
||||
return s.userRepo.List(ctx)
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
func (s *UserManagementService) UpdateUser(ctx context.Context, user *entity.User) error {
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// SetUserActive 启用/禁用用户
|
||||
func (s *UserManagementService) SetUserActive(ctx context.Context, userID string, isActive bool) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.IsActive = isActive
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// ChangeUserWorkspace 分配用户到 workspace
|
||||
func (s *UserManagementService) ChangeUserWorkspace(ctx context.Context, userID, workspaceID string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Admin 不能分配到 workspace
|
||||
if user.Role == entity.RoleAdmin {
|
||||
return fmt.Errorf("admin user cannot be assigned to workspace")
|
||||
}
|
||||
|
||||
// 验证 workspace 存在
|
||||
if workspaceID != "" {
|
||||
_, err := s.workspaceRepo.GetByID(ctx, workspaceID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows || err == entity.ErrWorkspaceNotFound {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user.WorkspaceID = workspaceID
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// ResetPassword 重置用户密码(Admin 操作)
|
||||
func (s *UserManagementService) ResetPassword(ctx context.Context, userID, newPassword string) error {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 哈希新密码
|
||||
passwordHash, err := s.passwordHasher.Hash(newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码并设置必须修改密码标志
|
||||
user.PasswordHash = passwordHash
|
||||
user.MustChangePassword = true
|
||||
user.RevokeAllTokens() // 强制登出所有会话
|
||||
|
||||
return s.userRepo.Update(ctx, user)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *UserManagementService) DeleteUser(ctx context.Context, id string) error {
|
||||
return s.userRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetUserWithWorkspace 获取用户及其 workspace 信息
|
||||
type UserWithWorkspace struct {
|
||||
User *entity.User
|
||||
Workspace *entity.Workspace
|
||||
}
|
||||
|
||||
// GetUserWithWorkspace 获取用户及其 workspace 信息
|
||||
func (s *UserManagementService) GetUserWithWorkspace(ctx context.Context, userID string) (*UserWithWorkspace, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &UserWithWorkspace{
|
||||
User: user,
|
||||
}
|
||||
|
||||
if user.WorkspaceID != "" {
|
||||
workspace, _ := s.workspaceRepo.GetByID(ctx, user.WorkspaceID)
|
||||
result.Workspace = workspace
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListUsersWithWorkspace 列出用户及其 workspace 信息
|
||||
func (s *UserManagementService) ListUsersWithWorkspace(ctx context.Context) ([]*UserWithWorkspace, error) {
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 预加载所有 workspace
|
||||
workspaces, err := s.workspaceRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workspaceMap := make(map[string]*entity.Workspace)
|
||||
for _, w := range workspaces {
|
||||
workspaceMap[w.ID] = w
|
||||
}
|
||||
|
||||
result := make([]*UserWithWorkspace, len(users))
|
||||
for i, user := range users {
|
||||
result[i] = &UserWithWorkspace{
|
||||
User: user,
|
||||
}
|
||||
if user.WorkspaceID != "" {
|
||||
result[i].Workspace = workspaceMap[user.WorkspaceID]
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EnsureAdminExists 确保存在一个 Admin 用户
|
||||
func (s *UserManagementService) EnsureAdminExists(ctx context.Context, defaultPassword string) error {
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否已有 admin
|
||||
for _, u := range users {
|
||||
if u.Role == entity.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认 admin 用户
|
||||
_, err = s.CreateUser(ctx, "admin", defaultPassword, "", string(entity.RoleAdmin), "")
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateInitialUser 创建初始用户(首次启动时调用)
|
||||
func (s *UserManagementService) CreateInitialUser(ctx context.Context, username, password, role string) (*entity.User, error) {
|
||||
// 检查是否已有用户
|
||||
users, err := s.userRepo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
return nil, fmt.Errorf("initial user already exists")
|
||||
}
|
||||
|
||||
// 验证角色
|
||||
if role != string(entity.RoleAdmin) && role != string(entity.RoleUser) {
|
||||
return nil, fmt.Errorf("invalid role: %s", role)
|
||||
}
|
||||
|
||||
// 哈希密码
|
||||
passwordHash, err := s.passwordHasher.Hash(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成占位邮箱
|
||||
email := username + "@local.ocdp"
|
||||
|
||||
// 创建用户
|
||||
user := entity.NewUser(username, passwordHash, email)
|
||||
user.ID = uuid.New().String()
|
||||
user.Role = entity.UserRole(role)
|
||||
// workspace_id 为 NULL(admin)或空(首个普通用户)
|
||||
user.IsActive = true
|
||||
user.MustChangePassword = false // 初始用户不需要强制修改密码
|
||||
|
||||
if err := user.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置创建时间和更新时间
|
||||
now := time.Now()
|
||||
user.CreatedAt = now
|
||||
user.UpdatedAt = now
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
Reference in New Issue
Block a user