241e3b5f2d
- Introduced a new `auditlog` package to handle audit logging for user actions, including creation, updates, deletions, and authentication events. - Enhanced existing services (customer, user) to log relevant actions using the new audit logger, capturing details such as actor ID, action type, target type, and success status. - Added middleware to enrich request context with metadata for audit logging, ensuring comprehensive tracking of user interactions. - Integrated Elasticsearch for persistent storage of audit logs, with fallback to file-only logging if Elasticsearch is unavailable. - Updated API documentation to include new audit log endpoints for administrative access. This update significantly improves the system's ability to track and audit user actions, enhancing security and accountability within the application.
829 lines
23 KiB
Go
829 lines
23 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"tm/pkg/audit"
|
|
"tm/pkg/authorization"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/notification"
|
|
"tm/pkg/redis"
|
|
"tm/pkg/response"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Service defines business logic for user operations
|
|
type Service interface {
|
|
// Main CRUD
|
|
Register(ctx context.Context, form *CreateUserForm) (*UserResponse, error)
|
|
Update(ctx context.Context, id string, form *UpdateUserForm) (*UserResponse, error)
|
|
UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error
|
|
GetUserByID(ctx context.Context, userID string) (*UserResponse, error)
|
|
GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error)
|
|
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error)
|
|
Delete(ctx context.Context, id string) error
|
|
AdminResetPassword(ctx context.Context, actorID, actorRole, targetID string) (*AdminResetPasswordResponse, error)
|
|
|
|
// Profile
|
|
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
|
UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error
|
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
|
ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error
|
|
ResetPassword(ctx context.Context, form *ResetPasswordForm) error
|
|
Logout(ctx context.Context, userID string, accessToken string) error
|
|
}
|
|
|
|
// userService implements the Service interface
|
|
type userService struct {
|
|
repository Repository
|
|
logger logger.Logger
|
|
authService authorization.AuthorizationService
|
|
validator ValidationService
|
|
notification notification.SDK
|
|
redisClient redis.Client
|
|
auditLogger audit.Logger
|
|
}
|
|
|
|
// NewService creates a new user service
|
|
func NewService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService, notificationSDK notification.SDK, redisClient redis.Client, auditLogger audit.Logger) Service {
|
|
return &userService{
|
|
repository: repository,
|
|
logger: logger,
|
|
authService: authService,
|
|
validator: validator,
|
|
notification: notificationSDK,
|
|
redisClient: redisClient,
|
|
auditLogger: auditLogger,
|
|
}
|
|
}
|
|
|
|
// Register creates a new user
|
|
func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*UserResponse, error) {
|
|
// Check if email already exists
|
|
existingUser, _ := s.repository.GetByEmail(ctx, form.Email)
|
|
if existingUser != nil {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
|
|
// Check if username already exists
|
|
existingUser, _ = s.repository.GetByUsername(ctx, strings.ToLower(form.Username))
|
|
if existingUser != nil {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
|
|
if form.Phone != nil && *form.Phone != "" {
|
|
existingUser, _ = s.repository.GetByPhone(ctx, *form.Phone)
|
|
if existingUser != nil {
|
|
return nil, errors.New("phone number already exists")
|
|
}
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to process password")
|
|
}
|
|
|
|
// Validate role
|
|
role := UserRole(form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return nil, errors.New("invalid role")
|
|
}
|
|
|
|
// Create user
|
|
user := &User{
|
|
FullName: form.FullName,
|
|
Username: strings.ToLower(form.Username),
|
|
Email: form.Email,
|
|
Password: string(hashedPassword),
|
|
Role: role,
|
|
Status: UserStatusActive,
|
|
Department: form.Department,
|
|
Position: form.Position,
|
|
Phone: form.Phone,
|
|
ProfileImage: form.ProfileImage,
|
|
IsVerified: true,
|
|
}
|
|
|
|
// Save to database
|
|
err = s.repository.Create(ctx, user)
|
|
if err != nil {
|
|
if mapped := mapUserWriteError(err); mapped != err {
|
|
return nil, mapped
|
|
}
|
|
s.logger.Error("Failed to register user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"email": form.Email,
|
|
"username": form.Username,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("User registered successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"username": user.Username,
|
|
"role": user.Role,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserCreate,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: user.ID.Hex(),
|
|
Success: true,
|
|
Metadata: map[string]string{"role": string(user.Role)},
|
|
})
|
|
|
|
go s.notification.SendEmail(context.Background(), notification.Model{
|
|
Recipient: form.Email,
|
|
Title: "Welcome to Opplens",
|
|
Message: fmt.Sprintf("Welcome to Opplens\nUsername: %s\nPassword: %s", strings.ToLower(form.Username), form.Password),
|
|
Type: "info",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// Update updates user information
|
|
func (s *userService) Update(ctx context.Context, id string, form *UpdateUserForm) (*UserResponse, error) {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.FullName != nil {
|
|
user.FullName = *form.FullName
|
|
}
|
|
|
|
if form.Username != nil {
|
|
// Check if username already exists
|
|
existingUser, _ := s.repository.GetByUsername(ctx, strings.ToLower(*form.Username))
|
|
if existingUser != nil && existingUser.ID.Hex() != id {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
user.Username = strings.ToLower(*form.Username)
|
|
}
|
|
|
|
if form.Email != nil {
|
|
// Check if email already exists
|
|
existingUser, _ := s.repository.GetByEmail(ctx, *form.Email)
|
|
if existingUser != nil && existingUser.ID.Hex() != id {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
user.Email = *form.Email
|
|
}
|
|
|
|
if form.Role != nil {
|
|
role := UserRole(*form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return nil, errors.New("invalid role")
|
|
}
|
|
user.Role = role
|
|
}
|
|
|
|
if form.Department != nil {
|
|
user.Department = form.Department
|
|
}
|
|
|
|
if form.Position != nil {
|
|
user.Position = form.Position
|
|
}
|
|
|
|
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
|
|
if *form.Phone != "" {
|
|
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
|
|
if existingUser != nil && existingUser.ID.Hex() != id {
|
|
return nil, errors.New("phone number already exists")
|
|
}
|
|
}
|
|
user.Phone = form.Phone
|
|
}
|
|
|
|
if form.isProfileImageSet() {
|
|
user.ProfileImage = form.ProfileImage
|
|
}
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
if mapped := mapUserWriteError(err); mapped != err {
|
|
return nil, mapped
|
|
}
|
|
s.logger.Error("Failed to update user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return nil, errors.New("failed to update user")
|
|
}
|
|
|
|
s.logger.Info("User updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserUpdate,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: id,
|
|
Success: true,
|
|
})
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// UpdateStatus updates user status
|
|
func (s *userService) UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Update status
|
|
user.Status = UserStatus(form.Status)
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
"status": form.Status,
|
|
})
|
|
return errors.New("failed to update user status")
|
|
}
|
|
|
|
s.logger.Info("User status updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
"status": form.Status,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserStatusChange,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: id,
|
|
Success: true,
|
|
Metadata: map[string]string{"status": form.Status},
|
|
})
|
|
|
|
go s.notification.SendEmail(context.Background(), notification.Model{
|
|
Recipient: user.Email,
|
|
Title: "Account Status Updated",
|
|
Message: fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)),
|
|
Type: "warning",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserByID retrieves a user by ID
|
|
func (s *userService) GetUserByID(ctx context.Context, id string) (*UserResponse, error) {
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
if err.Error() == "user not found" {
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserRead,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: id,
|
|
Success: false,
|
|
Metadata: map[string]string{"reason": "not_found"},
|
|
})
|
|
return nil, errors.New("user not found")
|
|
}
|
|
s.logger.Error("Failed to get user by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserRead,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: id,
|
|
Success: true,
|
|
})
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// GetUsersByIDs retrieves users by IDs
|
|
func (s *userService) GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error) {
|
|
users, err := s.repository.GetByIDs(ctx, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userResponses []*UserResponse
|
|
for _, user := range users {
|
|
userResponses = append(userResponses, user.ToResponse())
|
|
}
|
|
|
|
return &UserListResponse{
|
|
Users: userResponses,
|
|
}, nil
|
|
}
|
|
|
|
// Search searches users with search and filters
|
|
func (s *userService) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) {
|
|
// Get users
|
|
page, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list users", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
var userResponses []*UserResponse
|
|
for _, user := range page.Items {
|
|
userResponses = append(userResponses, user.ToResponse())
|
|
}
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserList,
|
|
Success: true,
|
|
Metadata: audit.MergeMetadata(
|
|
userSearchFilterMetadata(form),
|
|
audit.PaginationMetadata(pagination.Limit, pagination.Offset, page.TotalCount),
|
|
),
|
|
})
|
|
|
|
return &UserListResponse{
|
|
Users: userResponses,
|
|
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
|
|
}, nil
|
|
}
|
|
|
|
func userSearchFilterMetadata(form *SearchUsersForm) map[string]string {
|
|
if form == nil {
|
|
return nil
|
|
}
|
|
|
|
return audit.MergeMetadata(
|
|
audit.FlagMetadata("has_search", form.Search != nil && *form.Search != ""),
|
|
audit.OptionalStringMetadata("status", form.Status),
|
|
audit.OptionalStringMetadata("role", form.Role),
|
|
)
|
|
}
|
|
|
|
// Delete deletes a user (hard delete)
|
|
func (s *userService) Delete(ctx context.Context, id string) error {
|
|
err := s.repository.Delete(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to delete user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return errors.New("failed to delete user")
|
|
}
|
|
|
|
s.logger.Info("User deleted successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionUserDelete,
|
|
TargetType: audit.TargetTypeUser,
|
|
TargetID: id,
|
|
Success: true,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Login authenticates a user
|
|
func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) {
|
|
// Find user by email or username
|
|
var user *User
|
|
var err error
|
|
|
|
if strings.Contains(form.Username, "@") {
|
|
user, err = s.repository.GetByEmail(ctx, form.Username)
|
|
} else {
|
|
user, err = s.repository.GetByUsername(ctx, strings.ToLower(form.Username))
|
|
}
|
|
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with invalid credentials", map[string]interface{}{
|
|
"email_or_username": form.Username,
|
|
"error": err.Error(),
|
|
})
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
Action: audit.ActionAuthLoginFailure,
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
Success: false,
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// Check if user is active
|
|
if user.Status != UserStatusActive {
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: user.ID.Hex(),
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: user.ID.Hex(),
|
|
Action: audit.ActionAuthLoginFailure,
|
|
Success: false,
|
|
Metadata: map[string]string{"reason": "inactive"},
|
|
})
|
|
return nil, errors.New("account is inactive")
|
|
}
|
|
|
|
// Verify password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password))
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with wrong password", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: user.ID.Hex(),
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: user.ID.Hex(),
|
|
Action: audit.ActionAuthLoginFailure,
|
|
Success: false,
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// append device token to user if not exists
|
|
if !slices.Contains(user.DeviceToken, form.DeviceToken) {
|
|
user.DeviceToken = append(user.DeviceToken, form.DeviceToken)
|
|
}
|
|
|
|
// Update last login
|
|
err = s.repository.Login(ctx, user.ID.Hex())
|
|
if err != nil {
|
|
s.logger.Error("Failed to update last login", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": user.ID,
|
|
})
|
|
}
|
|
|
|
tokenPair, err := s.authService.GenerateTokenPair(
|
|
user.ID.Hex(),
|
|
user.Email,
|
|
string(user.Role),
|
|
"",
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": user.ID,
|
|
})
|
|
return nil, errors.New("failed to generate authentication tokens")
|
|
}
|
|
|
|
s.logger.Info("User logged in successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"role": user.Role,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: user.ID.Hex(),
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: user.ID.Hex(),
|
|
Action: audit.ActionAuthLoginSuccess,
|
|
Success: true,
|
|
Metadata: map[string]string{"role": string(user.Role)},
|
|
})
|
|
|
|
return &AuthResponse{
|
|
User: user.ToResponse(),
|
|
AccessToken: tokenPair.AccessToken,
|
|
RefreshToken: tokenPair.RefreshToken,
|
|
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
|
|
}, nil
|
|
}
|
|
|
|
// RefreshToken refreshes the access token
|
|
func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
|
|
// Validate refresh token and generate new access token
|
|
newAccessToken, err := s.authService.RefreshAccessToken(refreshToken)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to refresh access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid or expired refresh token")
|
|
}
|
|
|
|
// Parse the new access token to get user information
|
|
validationResult, err := s.authService.ValidateAccessToken(newAccessToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to validate new access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to generate valid access token")
|
|
}
|
|
|
|
if !validationResult.Valid {
|
|
s.logger.Error("New access token validation failed", map[string]interface{}{
|
|
"error": validationResult.Error,
|
|
})
|
|
return nil, errors.New("failed to generate valid access token")
|
|
}
|
|
|
|
// Get user information
|
|
userID, err := bson.ObjectIDFromHex(validationResult.Claims.UserID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse user ID from token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid token format")
|
|
}
|
|
|
|
user, err := s.repository.GetByID(ctx, userID.Hex())
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user for token refresh", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID.Hex(),
|
|
})
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
// Check if user is still active
|
|
if user.Status != UserStatusActive {
|
|
return nil, errors.New("user account is inactive")
|
|
}
|
|
|
|
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
|
|
return &AuthResponse{
|
|
User: user.ToResponse(),
|
|
AccessToken: newAccessToken,
|
|
RefreshToken: refreshToken, // Return the same refresh token
|
|
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
|
|
}, nil
|
|
}
|
|
|
|
// ChangePassword changes user password
|
|
func (s *userService) ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Verify old password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.OldPassword))
|
|
if err != nil {
|
|
return errors.New("invalid old password")
|
|
}
|
|
|
|
// Hash new password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash new password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to process new password")
|
|
}
|
|
|
|
// Update password
|
|
user.Password = string(hashedPassword)
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to update password")
|
|
}
|
|
|
|
s.logger.Info("Password changed successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: userID,
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: userID,
|
|
Action: audit.ActionAuthPasswordChange,
|
|
Success: true,
|
|
})
|
|
|
|
go s.notification.SendEmail(context.Background(), notification.Model{
|
|
Recipient: user.Email,
|
|
Title: "Password Reset Success",
|
|
Message: "Your password has been reset successfully",
|
|
Type: "info",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResetPassword initiates password reset process
|
|
func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm) error {
|
|
// Get user by email
|
|
user, err := s.repository.GetByEmail(ctx, form.Email)
|
|
if err != nil {
|
|
// Don't reveal if user exists or not for security
|
|
s.logger.Info("Password reset requested for non-existent email", map[string]interface{}{
|
|
"email": form.Email,
|
|
})
|
|
return nil // Return success even if user doesn't exist
|
|
}
|
|
|
|
// TODO: Implement password reset logic (send email with reset link)
|
|
s.logger.Info("Password reset requested", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: user.ID.Hex(),
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: user.ID.Hex(),
|
|
Action: audit.ActionAuthPasswordResetRequest,
|
|
Success: true,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetProfile retrieves the current user's profile
|
|
func (s *userService) GetProfile(ctx context.Context, userID string) (*User, error) {
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user profile", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
s.logger.Info("User profile retrieved successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// UpdateProfile updates the current user's profile
|
|
func (s *userService) UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error {
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user for profile update", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.FullName != nil {
|
|
user.FullName = *form.FullName
|
|
}
|
|
if form.Department != nil {
|
|
user.Department = form.Department
|
|
}
|
|
if form.Position != nil {
|
|
user.Position = form.Position
|
|
}
|
|
if form.Phone != nil && (user.Phone == nil || *form.Phone != *user.Phone) {
|
|
if *form.Phone != "" {
|
|
existingUser, _ := s.repository.GetByPhone(ctx, *form.Phone)
|
|
if existingUser != nil && existingUser.ID.Hex() != userID {
|
|
return errors.New("phone number already exists")
|
|
}
|
|
}
|
|
user.Phone = form.Phone
|
|
}
|
|
if form.isProfileImageSet() {
|
|
user.ProfileImage = form.ProfileImage
|
|
}
|
|
|
|
// Update timestamp
|
|
user.UpdatedAt = time.Now().Unix()
|
|
|
|
// Save to database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
if mapped := mapUserWriteError(err); mapped != err {
|
|
return mapped
|
|
}
|
|
s.logger.Error("Failed to update user profile", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to update profile")
|
|
}
|
|
|
|
s.logger.Info("User profile updated successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateUserRole updates user role
|
|
func (s *userService) UpdateRole(ctx context.Context, id string, form *UpdateUserRoleForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Validate role
|
|
role := UserRole(form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return errors.New("invalid role")
|
|
}
|
|
|
|
// Update role
|
|
user.Role = role
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user role", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
"role": form.Role,
|
|
})
|
|
return errors.New("failed to update user role")
|
|
}
|
|
|
|
s.logger.Info("User role updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
"role": form.Role,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Logout logs out a user and invalidates tokens
|
|
func (s *userService) Logout(ctx context.Context, userID string, accessToken string) error {
|
|
// get user by id
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user for logout", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
}
|
|
|
|
// Invalidate the access token
|
|
err = s.authService.InvalidateToken(accessToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to invalidate access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to invalidate access token")
|
|
}
|
|
|
|
// delete device token from user
|
|
user.DeviceToken = make([]string, 0)
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user device token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
}
|
|
|
|
s.logger.Info("User logged out successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: userID,
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeAdmin,
|
|
TargetID: userID,
|
|
Action: audit.ActionAuthLogout,
|
|
Success: true,
|
|
})
|
|
|
|
return nil
|
|
}
|