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:
Mazyar
2026-06-29 21:11:33 +03:30
parent 534213f10e
commit 241e3b5f2d
30 changed files with 1417 additions and 17 deletions
@@ -88,9 +88,11 @@ func (s *customerService) AdminResetPassword(ctx context.Context, actorID, targe
s.auditLogger.Log(ctx, audit.Entry{
ActorID: actorID,
ActorType: audit.ActorTypeAdmin,
TargetType: audit.TargetTypeCustomer,
TargetID: targetID,
Action: audit.ActionPasswordReset,
Success: true,
})
s.logger.Info("Customer password reset completed", map[string]interface{}{
+3
View File
@@ -3,6 +3,7 @@ package customer
import (
"net/http"
"strings"
"tm/pkg/audit"
"tm/pkg/response"
"github.com/labstack/echo/v4"
@@ -52,6 +53,8 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
c.Set("company_id", validationResult.Claims.CompanyID)
c.Set("access_token", tokenString)
audit.EnrichEchoContext(c, audit.ActorTypeCustomer)
// Continue to next handler
return next(c)
}
+111
View File
@@ -181,6 +181,14 @@ func (s *customerService) Register(ctx context.Context, form *CreateCustomerForm
"company_ids": Companies,
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerCreate,
TargetType: audit.TargetTypeCustomer,
TargetID: customer.ID.Hex(),
Success: true,
Metadata: map[string]string{"role": string(customer.Role)},
})
go s.notification.SendEmail(context.Background(), notification.Model{
Recipient: customer.Email,
Title: "Welcome to Opplens",
@@ -198,6 +206,13 @@ func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResp
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
if err.Error() == "customer not found" {
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerRead,
TargetType: audit.TargetTypeCustomer,
TargetID: id,
Success: false,
Metadata: map[string]string{"reason": "not_found"},
})
return nil, errors.New("customer not found")
}
s.logger.Error("Failed to get customer by ID", map[string]interface{}{
@@ -223,6 +238,13 @@ func (s *customerService) GetByID(ctx context.Context, id string) (*CustomerResp
"email": customer.Email,
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerRead,
TargetType: audit.TargetTypeCustomer,
TargetID: id,
Success: true,
})
return customer.ToResponse(companies), nil
}
@@ -249,12 +271,37 @@ func (s *customerService) Search(ctx context.Context, form *SearchCustomersForm,
customerResponses = append(customerResponses, customer.ToResponse(companies))
}
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerList,
Success: true,
Metadata: audit.MergeMetadata(
customerSearchFilterMetadata(form),
audit.PaginationMetadata(pagination.Limit, pagination.Offset, page.TotalCount),
),
})
return &CustomerListResponse{
Customers: customerResponses,
Meta: pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset),
}, nil
}
func customerSearchFilterMetadata(form *SearchCustomersForm) map[string]string {
if form == nil {
return nil
}
return audit.MergeMetadata(
audit.FlagMetadata("has_search", form.Search != nil && *form.Search != ""),
audit.FlagMetadata("has_full_name", form.FullName != nil && *form.FullName != ""),
audit.FlagMetadata("has_email_filter", form.Email != nil && *form.Email != ""),
audit.OptionalStringMetadata("status", form.Status),
audit.OptionalStringMetadata("role", form.Role),
audit.OptionalStringMetadata("type", form.Type),
audit.OptionalStringMetadata("company_id", form.CompanyID),
)
}
func (s *customerService) SearchNotification(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) ([]*CustomerNotificationResponse, error) {
page, err := s.repository.Search(ctx, form, pagination)
if err != nil {
@@ -452,6 +499,13 @@ func (s *customerService) Update(ctx context.Context, id string, form *UpdateCus
"email": customer.Email,
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerUpdate,
TargetType: audit.TargetTypeCustomer,
TargetID: id,
Success: true,
})
responseCompanies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer response", map[string]interface{}{
@@ -490,6 +544,13 @@ func (s *customerService) Delete(ctx context.Context, id string) error {
"customer_id": id,
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerDelete,
TargetType: audit.TargetTypeCustomer,
TargetID: id,
Success: true,
})
return nil
}
@@ -518,6 +579,14 @@ func (s *customerService) UpdateStatus(ctx context.Context, id string, form *Upd
"status": form.Status,
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionCustomerStatusChange,
TargetType: audit.TargetTypeCustomer,
TargetID: id,
Success: true,
Metadata: map[string]string{"status": form.Status},
})
go s.notification.SendEmail(context.Background(), notification.Model{
Recipient: customer.Email,
Title: "Account Status Updated",
@@ -682,6 +751,12 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
s.logger.Warn("Customer login failed - username not found", map[string]interface{}{
"username": strings.ToLower(form.Username),
})
s.auditLogger.Log(ctx, audit.Entry{
Action: audit.ActionAuthLoginFailure,
ActorType: audit.ActorTypeCustomer,
TargetType: audit.TargetTypeCustomer,
Success: false,
})
return nil, errors.New("invalid credentials")
}
@@ -692,6 +767,15 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
"customer_id": customer.ID,
"status": string(customer.Status),
})
s.auditLogger.Log(ctx, audit.Entry{
ActorID: customer.ID.Hex(),
ActorType: audit.ActorTypeCustomer,
TargetType: audit.TargetTypeCustomer,
TargetID: customer.ID.Hex(),
Action: audit.ActionAuthLoginFailure,
Success: false,
Metadata: map[string]string{"reason": "inactive"},
})
return nil, errors.New("account is not active")
}
@@ -702,6 +786,14 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
"customer_id": customer.ID,
"email": customer.Email,
})
s.auditLogger.Log(ctx, audit.Entry{
ActorID: customer.ID.Hex(),
ActorType: audit.ActorTypeCustomer,
TargetType: audit.TargetTypeCustomer,
TargetID: customer.ID.Hex(),
Action: audit.ActionAuthLoginFailure,
Success: false,
})
return nil, errors.New("invalid credentials")
}
@@ -747,6 +839,16 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
"customer_type": string(customer.Type),
})
s.auditLogger.Log(ctx, audit.Entry{
ActorID: customer.ID.Hex(),
ActorType: audit.ActorTypeCustomer,
TargetType: audit.TargetTypeCustomer,
TargetID: customer.ID.Hex(),
Action: audit.ActionAuthLoginSuccess,
Success: true,
Metadata: map[string]string{"role": string(customer.Role)},
})
return &AuthResponse{
Customer: customer.ToResponse(companies),
AccessToken: tokenPair.AccessToken,
@@ -942,6 +1044,15 @@ func (s *customerService) Logout(ctx context.Context, customerID string, accessT
"customer_id": customerID,
})
s.auditLogger.Log(ctx, audit.Entry{
ActorID: customerID,
ActorType: audit.ActorTypeCustomer,
TargetType: audit.TargetTypeCustomer,
TargetID: customerID,
Action: audit.ActionAuthLogout,
Success: true,
})
return nil
}