321 lines
8.9 KiB
Go
321 lines
8.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"omega-server/local/model"
|
|
"omega-server/local/repository"
|
|
"omega-server/local/utl/jwt"
|
|
"omega-server/local/utl/logging"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// CacheInvalidator interface for cache invalidation
|
|
type CacheInvalidator interface {
|
|
InvalidateUserPermissions(userID string)
|
|
InvalidateAllUserPermissions()
|
|
}
|
|
|
|
// MembershipService provides business logic for membership-related operations.
|
|
type MembershipService struct {
|
|
repo *repository.MembershipRepository
|
|
cacheInvalidator CacheInvalidator
|
|
}
|
|
|
|
// NewMembershipService creates a new MembershipService.
|
|
func NewMembershipService(repo *repository.MembershipRepository) *MembershipService {
|
|
return &MembershipService{
|
|
repo: repo,
|
|
cacheInvalidator: nil, // Will be set later via SetCacheInvalidator
|
|
}
|
|
}
|
|
|
|
// SetCacheInvalidator sets the cache invalidator after service initialization
|
|
func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
|
|
s.cacheInvalidator = invalidator
|
|
}
|
|
|
|
// Login authenticates a user and returns a JWT.
|
|
func (s *MembershipService) Login(ctx context.Context, email, password string) (string, error) {
|
|
user, err := s.repo.FindUserByEmail(ctx, email)
|
|
if err != nil {
|
|
return "", errors.New("invalid credentials")
|
|
}
|
|
|
|
// Use secure password verification with constant-time comparison
|
|
if !user.VerifyPassword(password) {
|
|
return "", errors.New("invalid credentials")
|
|
}
|
|
|
|
// Extract role names for JWT
|
|
roleNames := make([]string, len(user.Roles))
|
|
for i, role := range user.Roles {
|
|
roleNames[i] = role.Name
|
|
}
|
|
|
|
return jwt.GenerateToken(user.ID, user.Email, user.FullName, roleNames)
|
|
}
|
|
|
|
// CreateUser creates a new user.
|
|
func (s *MembershipService) CreateUser(ctx context.Context, user *model.User, roleIDs []string) (*model.User, error) {
|
|
// Validate domain model
|
|
if err := user.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle roles
|
|
if len(roleIDs) > 0 {
|
|
roles := make([]model.Role, 0, len(roleIDs))
|
|
for _, roleID := range roleIDs {
|
|
role, err := s.repo.FindRoleByName(ctx, roleID)
|
|
if err != nil {
|
|
logging.Error("Failed to find role by name: %v", err)
|
|
return nil, errors.New("role not found: " + roleID)
|
|
}
|
|
roles = append(roles, *role)
|
|
}
|
|
user.Roles = roles
|
|
}
|
|
|
|
// Create user
|
|
if err := s.repo.CreateUser(ctx, user); err != nil {
|
|
logging.Error("Failed to create user: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Log with role names
|
|
roleNames := make([]string, len(user.Roles))
|
|
for i, role := range user.Roles {
|
|
roleNames[i] = role.Name
|
|
}
|
|
logging.InfoOperation("USER_CREATE", "Created user: "+user.Email+" (ID: "+user.ID+", Roles: "+strings.Join(roleNames, ", ")+")")
|
|
return user, nil
|
|
}
|
|
|
|
// ListUsers retrieves all users.
|
|
func (s *MembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
|
|
return s.repo.ListUsers(ctx)
|
|
}
|
|
|
|
// GetUser retrieves a single user by ID.
|
|
func (s *MembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
|
|
return s.repo.FindUserByID(ctx, userID)
|
|
}
|
|
|
|
// GetUserWithPermissions retrieves a single user by ID with their role and permissions.
|
|
func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
|
|
return s.repo.FindUserByIDWithPermissions(ctx, userID)
|
|
}
|
|
|
|
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
|
|
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
|
// Get user with role information
|
|
user, err := s.repo.FindUserByID(ctx, userID)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Prevent deletion of Super Admin users
|
|
for _, role := range user.Roles {
|
|
if role.Name == "Super Admin" {
|
|
return errors.New("cannot delete Super Admin user")
|
|
}
|
|
}
|
|
|
|
err = s.repo.DeleteUser(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Invalidate cache for deleted user
|
|
if s.cacheInvalidator != nil {
|
|
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
|
}
|
|
|
|
logging.InfoOperation("USER_DELETE", "Deleted user: "+userID.String())
|
|
return nil
|
|
}
|
|
|
|
// UpdateUser updates a user's details.
|
|
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req *model.UserUpdateRequest) (*model.User, error) {
|
|
// Validate request
|
|
if err := req.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.repo.FindUserByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
// Apply update request to user
|
|
if err := req.ApplyToUser(user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Handle roles if provided
|
|
if len(req.RoleIDs) > 0 {
|
|
roles := make([]model.Role, 0, len(req.RoleIDs))
|
|
for _, roleID := range req.RoleIDs {
|
|
roleUUID, err := uuid.Parse(roleID)
|
|
if err != nil {
|
|
return nil, errors.New("invalid role ID format: " + roleID)
|
|
}
|
|
role, err := s.repo.FindRoleByID(ctx, roleUUID)
|
|
if err != nil {
|
|
return nil, errors.New("role not found: " + roleID)
|
|
}
|
|
roles = append(roles, *role)
|
|
}
|
|
user.Roles = roles
|
|
}
|
|
|
|
// Validate updated user
|
|
if err := user.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.repo.UpdateUser(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Invalidate cache if role was changed
|
|
if len(req.RoleIDs) > 0 && s.cacheInvalidator != nil {
|
|
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
|
|
}
|
|
|
|
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Email+" (ID: "+user.ID+")")
|
|
return user, nil
|
|
}
|
|
|
|
// HasPermission checks if a user has a specific permission.
|
|
func (s *MembershipService) HasPermission(ctx context.Context, userID string, permissionName string) (bool, error) {
|
|
user, err := s.repo.FindUserByIDWithPermissions(ctx, userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Check all user roles for permissions
|
|
for _, role := range user.Roles {
|
|
// Super admin and Admin have all permissions
|
|
if role.Name == "Super Admin" || role.Name == "Admin" {
|
|
return true, nil
|
|
}
|
|
|
|
for _, p := range role.Permissions {
|
|
if p.Name == permissionName {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// SetupInitialData creates the initial roles and permissions.
|
|
func (s *MembershipService) SetupInitialData(ctx context.Context) error {
|
|
// Define all permissions
|
|
permissions := model.AllPermissions
|
|
|
|
createdPermissions := make([]model.Permission, 0)
|
|
for _, pName := range permissions {
|
|
perm, err := s.repo.FindPermissionByName(ctx, pName)
|
|
if err != nil { // Assuming error means not found
|
|
perm = &model.Permission{Name: pName}
|
|
if err := s.repo.CreatePermission(ctx, perm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
createdPermissions = append(createdPermissions, *perm)
|
|
}
|
|
|
|
// Create Super Admin role with all permissions
|
|
superAdminRole, err := s.repo.FindRoleByName(ctx, "Super Admin")
|
|
if err != nil {
|
|
superAdminRole = &model.Role{Name: "Super Admin"}
|
|
if err := s.repo.CreateRole(ctx, superAdminRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := s.repo.AssignPermissionsToRole(ctx, superAdminRole, createdPermissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create Admin role with same permissions as Super Admin
|
|
adminRole, err := s.repo.FindRoleByName(ctx, "Admin")
|
|
if err != nil {
|
|
adminRole = &model.Role{Name: "Admin"}
|
|
if err := s.repo.CreateRole(ctx, adminRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := s.repo.AssignPermissionsToRole(ctx, adminRole, createdPermissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create Manager role with limited permissions (excluding membership, role, user, server create/delete)
|
|
managerRole, err := s.repo.FindRoleByName(ctx, "Manager")
|
|
if err != nil {
|
|
managerRole = &model.Role{Name: "Manager"}
|
|
if err := s.repo.CreateRole(ctx, managerRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Define manager permissions (limited set)
|
|
managerPermissionNames := []string{
|
|
model.ServerView,
|
|
model.ServerUpdate,
|
|
model.ServerStart,
|
|
model.ServerStop,
|
|
model.ConfigView,
|
|
model.ConfigUpdate,
|
|
}
|
|
|
|
managerPermissions := make([]model.Permission, 0)
|
|
for _, permName := range managerPermissionNames {
|
|
for _, perm := range createdPermissions {
|
|
if perm.Name == permName {
|
|
managerPermissions = append(managerPermissions, perm)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := s.repo.AssignPermissionsToRole(ctx, managerRole, managerPermissions); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Invalidate all caches after role setup changes
|
|
if s.cacheInvalidator != nil {
|
|
s.cacheInvalidator.InvalidateAllUserPermissions()
|
|
}
|
|
|
|
// Create a default admin user if one doesn't exist
|
|
_, err = s.repo.FindUserByEmail(ctx, "admin@example.com")
|
|
if err != nil {
|
|
logging.Debug("Creating default admin user")
|
|
adminUser := &model.User{
|
|
Email: "admin@example.com",
|
|
FullName: "System Administrator",
|
|
}
|
|
if err := adminUser.SetPassword(os.Getenv("PASSWORD")); err != nil {
|
|
return err
|
|
}
|
|
_, err = s.CreateUser(ctx, adminUser, []string{"Super Admin"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAllRoles retrieves all roles for dropdown selection.
|
|
func (s *MembershipService) GetAllRoles(ctx context.Context) ([]*model.Role, error) {
|
|
return s.repo.ListRoles(ctx)
|
|
}
|