Implement audit logging functionality across services and middleware
- 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.
This commit is contained in:
@@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"tm/pkg/audit"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -59,6 +60,8 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
|
||||
c.Set("company_id", validationResult.Claims.CompanyID)
|
||||
c.Set("access_token", tokenString)
|
||||
|
||||
audit.EnrichEchoContext(c, audit.ActorTypeAdmin)
|
||||
|
||||
// Continue to next handler
|
||||
return next(c)
|
||||
}
|
||||
|
||||
@@ -93,9 +93,11 @@ func (s *userService) AdminResetPassword(ctx context.Context, actorID, actorRole
|
||||
|
||||
s.auditLogger.Log(ctx, audit.Entry{
|
||||
ActorID: actorID,
|
||||
ActorType: audit.ActorTypeAdmin,
|
||||
TargetType: audit.TargetTypeAdmin,
|
||||
TargetID: targetID,
|
||||
Action: audit.ActionPasswordReset,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
s.logger.Info("Admin password reset completed", map[string]interface{}{
|
||||
|
||||
@@ -135,6 +135,14 @@ func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*User
|
||||
"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",
|
||||
@@ -225,6 +233,13 @@ func (s *userService) Update(ctx context.Context, id string, form *UpdateUserFor
|
||||
"user_id": id,
|
||||
})
|
||||
|
||||
s.auditLogger.Log(ctx, audit.Entry{
|
||||
Action: audit.ActionUserUpdate,
|
||||
TargetType: audit.TargetTypeUser,
|
||||
TargetID: id,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
@@ -255,6 +270,14 @@ func (s *userService) UpdateStatus(ctx context.Context, id string, form *UpdateU
|
||||
"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",
|
||||
@@ -272,6 +295,13 @@ func (s *userService) GetUserByID(ctx context.Context, id string) (*UserResponse
|
||||
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{}{
|
||||
@@ -281,6 +311,13 @@ func (s *userService) GetUserByID(ctx context.Context, id string) (*UserResponse
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.auditLogger.Log(ctx, audit.Entry{
|
||||
Action: audit.ActionUserRead,
|
||||
TargetType: audit.TargetTypeUser,
|
||||
TargetID: id,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
@@ -317,12 +354,33 @@ func (s *userService) Search(ctx context.Context, form *SearchUsersForm, paginat
|
||||
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)
|
||||
@@ -338,6 +396,13 @@ func (s *userService) Delete(ctx context.Context, id string) error {
|
||||
"user_id": id,
|
||||
})
|
||||
|
||||
s.auditLogger.Log(ctx, audit.Entry{
|
||||
Action: audit.ActionUserDelete,
|
||||
TargetType: audit.TargetTypeUser,
|
||||
TargetID: id,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -358,11 +423,26 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
|
||||
"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")
|
||||
}
|
||||
|
||||
@@ -373,6 +453,14 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
|
||||
"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")
|
||||
}
|
||||
|
||||
@@ -410,6 +498,16 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
|
||||
"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,
|
||||
@@ -520,6 +618,15 @@ func (s *userService) ChangePassword(ctx context.Context, userID string, form *C
|
||||
"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",
|
||||
@@ -550,6 +657,15 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -699,5 +815,14 @@ func (s *userService) Logout(ctx context.Context, userID string, accessToken str
|
||||
"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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user