Add UserService (GetMe, UpdateMe, admin CRUD with block/unblock), UserHandler (/users, /users/me), ACLHandler (GET/PUT /acl/:type/:id), AuditHandler (GET /audit with all filters). Fix UserRepo.Update to include is_blocked. Wire all remaining routes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
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
|
|
} |