package service import ( "context" "fmt" "golang.org/x/crypto/bcrypt" "tanabata/backend/internal/domain" "tanabata/backend/internal/port" ) // UserService handles user CRUD and profile management. type UserService struct { users port.UserRepo audit *AuditService } // NewUserService creates a UserService. func NewUserService(users port.UserRepo, audit *AuditService) *UserService { return &UserService{users: users, audit: audit} } // --------------------------------------------------------------------------- // Self-service // --------------------------------------------------------------------------- // GetMe returns the profile of the currently authenticated user. func (s *UserService) GetMe(ctx context.Context) (*domain.User, error) { userID, _, _ := domain.UserFromContext(ctx) return s.users.GetByID(ctx, userID) } // UpdateMeParams holds fields a user may change on their own profile. type UpdateMeParams struct { Name string // empty = no change Password *string // nil = no change } // UpdateMe allows a user to change their own name and/or password. func (s *UserService) UpdateMe(ctx context.Context, p UpdateMeParams) (*domain.User, error) { userID, _, _ := domain.UserFromContext(ctx) current, err := s.users.GetByID(ctx, userID) if err != nil { return nil, err } patch := *current if p.Name != "" { patch.Name = p.Name } if p.Password != nil { hash, err := bcrypt.GenerateFromPassword([]byte(*p.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("UserService.UpdateMe hash: %w", err) } patch.Password = string(hash) } return s.users.Update(ctx, userID, &patch) } // --------------------------------------------------------------------------- // Admin CRUD // --------------------------------------------------------------------------- // List returns a paginated list of users (admin only — caller must enforce). func (s *UserService) List(ctx context.Context, params port.OffsetParams) (*domain.UserPage, error) { return s.users.List(ctx, params) } // Get returns a user by ID (admin only). func (s *UserService) Get(ctx context.Context, id int16) (*domain.User, error) { return s.users.GetByID(ctx, id) } // CreateParams holds fields for creating a new user. type CreateUserParams struct { Name string Password string IsAdmin bool CanCreate bool } // Create inserts a new user with a bcrypt-hashed password (admin only). func (s *UserService) Create(ctx context.Context, p CreateUserParams) (*domain.User, error) { hash, err := bcrypt.GenerateFromPassword([]byte(p.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("UserService.Create hash: %w", err) } u := &domain.User{ Name: p.Name, Password: string(hash), IsAdmin: p.IsAdmin, CanCreate: p.CanCreate, } created, err := s.users.Create(ctx, u) if err != nil { return nil, err } _ = s.audit.Log(ctx, "user_create", nil, nil, map[string]any{"target_user_id": created.ID}) return created, nil } // UpdateAdminParams holds fields an admin may change on any user. type UpdateAdminParams struct { IsAdmin *bool CanCreate *bool IsBlocked *bool } // UpdateAdmin applies an admin-level patch to a user. func (s *UserService) UpdateAdmin(ctx context.Context, id int16, p UpdateAdminParams) (*domain.User, error) { current, err := s.users.GetByID(ctx, id) if err != nil { return nil, err } patch := *current if p.IsAdmin != nil { patch.IsAdmin = *p.IsAdmin } if p.CanCreate != nil { patch.CanCreate = *p.CanCreate } if p.IsBlocked != nil { patch.IsBlocked = *p.IsBlocked } updated, err := s.users.Update(ctx, id, &patch) if err != nil { return nil, err } // Log block/unblock specifically. if p.IsBlocked != nil { action := "user_unblock" if *p.IsBlocked { action = "user_block" } _ = s.audit.Log(ctx, action, nil, nil, map[string]any{"target_user_id": id}) } return updated, nil } // Delete removes a user by ID (admin only). func (s *UserService) Delete(ctx context.Context, id int16) error { if err := s.users.Delete(ctx, id); err != nil { return err } _ = s.audit.Log(ctx, "user_delete", nil, nil, map[string]any{"target_user_id": id}) return nil }