Cybersecurity-Projects/PROJECTS/advanced/monitor-the-situation-dashb.../backend/internal/user/service.go

351 lines
7.1 KiB
Go

// AngelaMos | 2026
// service.go
package user
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/carterperez-dev/monitor-the-situation/backend/internal/auth"
"github.com/carterperez-dev/monitor-the-situation/backend/internal/core"
)
type Service struct {
repo Repository
}
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) GetByID(
ctx context.Context,
id string,
) (*auth.UserInfo, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return toUserInfo(user), nil
}
func (s *Service) GetByEmail(
ctx context.Context,
email string,
) (*auth.UserInfo, error) {
user, err := s.repo.GetByEmail(ctx, strings.ToLower(email))
if err != nil {
return nil, err
}
return toUserInfo(user), nil
}
func (s *Service) Create(
ctx context.Context,
email, passwordHash, name string,
) (*auth.UserInfo, error) {
user := &User{
ID: uuid.New().String(),
Email: strings.ToLower(email),
PasswordHash: passwordHash,
Name: name,
Role: RoleUser,
Tier: TierFree,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
return toUserInfo(user), nil
}
func (s *Service) IncrementTokenVersion(
ctx context.Context,
userID string,
) error {
return s.repo.IncrementTokenVersion(ctx, userID)
}
func (s *Service) UpdatePassword(
ctx context.Context,
userID, passwordHash string,
) error {
return s.repo.UpdatePassword(ctx, userID, passwordHash)
}
// SetRole flips a user's role. Used by ADMIN_EMAIL bootstrapping in
// auth.Service: the configured email gets promoted to admin on every
// login/register so a fresh DB after env changes still has an admin.
func (s *Service) SetRole(ctx context.Context, userID, role string) error {
if role != RoleUser && role != RoleAdmin {
return fmt.Errorf(
"set role: invalid role %q: %w",
role,
core.ErrInvalidInput,
)
}
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return err
}
user.Role = role
return s.repo.Update(ctx, user)
}
// UpdateEmail changes a user's email after re-verifying their password.
// Email is normalized to lowercase. Returns ErrDuplicateKey if the new
// email is already taken by another account.
func (s *Service) UpdateEmail(
ctx context.Context,
userID, currentPassword, newEmail string,
) (*User, error) {
newEmail = strings.ToLower(strings.TrimSpace(newEmail))
if newEmail == "" {
return nil, fmt.Errorf("update email: empty: %w", core.ErrInvalidInput)
}
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
valid, _, err := core.VerifyPasswordWithRehash(
currentPassword,
user.PasswordHash,
)
if err != nil {
return nil, fmt.Errorf("verify password: %w", err)
}
if !valid {
return nil, fmt.Errorf("update email: %w", core.ErrUnauthorized)
}
if user.Email == newEmail {
return user, nil
}
if err := s.repo.UpdateEmail(ctx, userID, newEmail); err != nil {
return nil, err
}
user.Email = newEmail
return user, nil
}
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.GetByID(ctx, id)
}
func (s *Service) UpdateUser(
ctx context.Context,
id string,
req UpdateUserRequest,
) (*User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
user.Name = *req.Name
}
if err := s.repo.Update(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *Service) AdminUpdateUser(
ctx context.Context,
id string,
req AdminUpdateUserRequest,
) (*User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
user.Name = *req.Name
}
if req.Role != nil {
if *req.Role != RoleUser && *req.Role != RoleAdmin {
return nil, fmt.Errorf(
"update role: invalid role %q: %w",
*req.Role,
core.ErrInvalidInput,
)
}
user.Role = *req.Role
}
if req.Tier != nil {
if *req.Tier != TierFree && *req.Tier != TierPro &&
*req.Tier != TierEnterprise {
return nil, fmt.Errorf(
"update tier: invalid tier %q: %w",
*req.Tier,
core.ErrInvalidInput,
)
}
user.Tier = *req.Tier
}
if err := s.repo.Update(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *Service) AdminCreateUser(
ctx context.Context,
req AdminCreateUserRequest,
) (*User, error) {
hash, err := core.HashPassword(req.Password)
if err != nil {
return nil, fmt.Errorf("hash password: %w", err)
}
user := &User{
ID: uuid.New().String(),
Email: strings.ToLower(req.Email),
PasswordHash: hash,
Name: req.Name,
Role: RoleUser,
Tier: TierFree,
}
if req.Role != nil {
if *req.Role != RoleUser && *req.Role != RoleAdmin {
return nil, fmt.Errorf(
"create user: invalid role %q: %w",
*req.Role,
core.ErrInvalidInput,
)
}
user.Role = *req.Role
}
if req.Tier != nil {
if *req.Tier != TierFree && *req.Tier != TierPro &&
*req.Tier != TierEnterprise {
return nil, fmt.Errorf(
"create user: invalid tier %q: %w",
*req.Tier,
core.ErrInvalidInput,
)
}
user.Tier = *req.Tier
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (s *Service) DeleteUser(ctx context.Context, id string) error {
return s.repo.SoftDelete(ctx, id)
}
func (s *Service) ListUsers(
ctx context.Context,
params ListUsersParams,
) ([]User, int, error) {
return s.repo.List(ctx, params)
}
func (s *Service) GetMe(ctx context.Context, userID string) (*User, error) {
if userID == "" {
return nil, fmt.Errorf("get me: %w", core.ErrUnauthorized)
}
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return nil, err
}
return user, nil
}
func (s *Service) UpdateMe(
ctx context.Context,
userID string,
req UpdateUserRequest,
) (*User, error) {
if userID == "" {
return nil, fmt.Errorf("update me: %w", core.ErrUnauthorized)
}
return s.UpdateUser(ctx, userID, req)
}
func (s *Service) DeleteMe(ctx context.Context, userID string) error {
if userID == "" {
return fmt.Errorf("delete me: %w", core.ErrUnauthorized)
}
return s.repo.SoftDelete(ctx, userID)
}
func (s *Service) EmailExists(
ctx context.Context,
email string,
) (bool, error) {
exists, err := s.repo.ExistsByEmail(ctx, email)
if err != nil {
return false, err
}
return exists, nil
}
func (s *Service) CanDeleteUser(
ctx context.Context,
requesterID, targetID string,
) error {
if requesterID == targetID {
return nil
}
requester, err := s.repo.GetByID(ctx, requesterID)
if err != nil {
return err
}
if !requester.IsAdmin() {
return fmt.Errorf("delete user: %w", core.ErrForbidden)
}
target, err := s.repo.GetByID(ctx, targetID)
if err != nil {
return err
}
if target.IsAdmin() {
return fmt.Errorf("cannot delete admin users: %w", core.ErrForbidden)
}
return nil
}
func toUserInfo(u *User) *auth.UserInfo {
return &auth.UserInfo{
ID: u.ID,
Email: u.Email,
Name: u.Name,
PasswordHash: u.PasswordHash,
Role: u.Role,
Tier: u.Tier,
TokenVersion: u.TokenVersion,
}
}
var _ auth.UserProvider = (*Service)(nil)