Skip to content

Commit

Permalink
[SDP-1032] Add logs for any changes in user profile or the organizati…
Browse files Browse the repository at this point in the history
…on profile (#145)

### What

Start logging important changes on user or organization profiles, for traceability. Here are the functions that are now being logged:

- Changes made through `PatchOrganizationProfile`
  - Log message: `log.Ctx(ctx).Warnf("[PatchOrganizationProfile] - userID %s will update the organization fields %v", user.ID, nonEmptyKeys)`
- Changes made through `PatchUserProfile`
  - Log message: `log.Ctx(ctx).Warnf("[PatchUserProfile] - Will update email for userID %s to %s", user.ID, utils.TruncateString(reqBody.Email, 3))`
- Changes made through `PatchUserPassword`
  - Log message: `log.Ctx(ctx).Warnf("[UpdateUserPassword] - Will update password for user account ID %s", user.ID)`

Also, refactored some tests.

### Why

So we can better track changes made in user profiles or Organization profiles, for accountability.
  • Loading branch information
marcelosalloum authored Jan 12, 2024
1 parent fe3e148 commit ce0d6a8
Show file tree
Hide file tree
Showing 3 changed files with 777 additions and 1,068 deletions.
16 changes: 8 additions & 8 deletions internal/data/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ type Organization struct {
}

type OrganizationUpdate struct {
Name string
Logo []byte
TimezoneUTCOffset string
IsApprovalRequired *bool
SMSResendInterval *int64
PaymentCancellationPeriodDays *int64
Name string `json:",omitempty"`
Logo []byte `json:",omitempty"`
TimezoneUTCOffset string `json:",omitempty"`
IsApprovalRequired *bool `json:",omitempty"`
SMSResendInterval *int64 `json:",omitempty"`
PaymentCancellationPeriodDays *int64 `json:",omitempty"`

// Using pointers to accept empty strings
SMSRegistrationMessageTemplate *string
OTPMessageTemplate *string
SMSRegistrationMessageTemplate *string `json:",omitempty"`
OTPMessageTemplate *string `json:",omitempty"`
}

type LogoType string
Expand Down
118 changes: 66 additions & 52 deletions internal/serve/httphandler/profile_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package httphandler

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"image"
"sort"

// Don't remove the `image/jpeg` and `image/png` packages import unless
// the `image` package is no longer necessary.
Expand Down Expand Up @@ -69,7 +71,6 @@ type PatchUserProfileRequest struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
}

type GetProfileResponse struct {
Expand All @@ -89,9 +90,9 @@ type PatchUserPasswordRequest struct {
func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

_, ok := ctx.Value(middleware.TokenContextKey).(string)
if !ok {
httperror.Unauthorized("", nil, nil).Render(rw)
_, user, httpErr := getTokenAndUser(ctx, h.AuthManager)
if httpErr != nil {
httpErr.Render(rw)
return
}

Expand Down Expand Up @@ -154,7 +155,7 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
return
}

err = h.Models.Organizations.Update(ctx, &data.OrganizationUpdate{
organizationUpdate := data.OrganizationUpdate{
Name: reqBody.OrganizationName,
Logo: fileContentBytes,
TimezoneUTCOffset: reqBody.TimezoneUTCOffset,
Expand All @@ -163,7 +164,26 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
OTPMessageTemplate: reqBody.OTPMessageTemplate,
SMSResendInterval: reqBody.SMSResendInterval,
PaymentCancellationPeriodDays: reqBody.PaymentCancellationPeriodDays,
})
}
requestDict, err := utils.ConvertType[data.OrganizationUpdate, map[string]interface{}](organizationUpdate)
if err != nil {
httperror.InternalError(ctx, "Cannot convert organization update to map", err, nil).Render(rw)
return
}
var nonEmptyChanges []string
for k, v := range requestDict {
if !utils.IsEmpty(v) {
value := v
if k == "Logo" {
value = "..."
}
nonEmptyChanges = append(nonEmptyChanges, fmt.Sprintf("%s='%v'", k, value))
}
}
sort.Strings(nonEmptyChanges)

log.Ctx(ctx).Warnf("[PatchOrganizationProfile] - userID %s will update the organization fields [%s]", user.ID, strings.Join(nonEmptyChanges, ", "))
err = h.Models.Organizations.Update(ctx, &organizationUpdate)
if err != nil {
httperror.InternalError(ctx, "Cannot update organization", err, nil).Render(rw)
return
Expand All @@ -175,9 +195,9 @@ func (h ProfileHandler) PatchOrganizationProfile(rw http.ResponseWriter, req *ht
func (h ProfileHandler) PatchUserProfile(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

token, ok := ctx.Value(middleware.TokenContextKey).(string)
if !ok {
httperror.Unauthorized("", nil, nil).Render(rw)
token, user, httpErr := getTokenAndUser(ctx, h.AuthManager)
if httpErr != nil {
httpErr.Render(rw)
return
}

Expand All @@ -189,30 +209,24 @@ func (h ProfileHandler) PatchUserProfile(rw http.ResponseWriter, req *http.Reque
return
}

if reqBody.Password != "" && len(reqBody.Password) < 8 {
httperror.BadRequest("", nil, map[string]interface{}{
"password": "password should have at least 8 characters",
}).Render(rw)
return
}

if reqBody.Email != "" {
if err := utils.ValidateEmail(reqBody.Email); err != nil {
httperror.BadRequest("", nil, map[string]interface{}{
"email": "invalid email provided",
}).Render(rw)
return
}
log.Ctx(ctx).Warnf("[PatchUserProfile] - Will update email for userID %s to %s", user.ID, utils.TruncateString(reqBody.Email, 3))
}

if reqBody.FirstName == "" && reqBody.LastName == "" && reqBody.Email == "" && reqBody.Password == "" {
if utils.IsEmpty(reqBody) {
httperror.BadRequest("", nil, map[string]interface{}{
"details": "provide at least first_name, last_name, email or password.",
"details": "provide at least first_name, last_name or email.",
}).Render(rw)
return
}

err := h.AuthManager.UpdateUser(ctx, token, reqBody.FirstName, reqBody.LastName, reqBody.Email, reqBody.Password)
err := h.AuthManager.UpdateUser(ctx, token, reqBody.FirstName, reqBody.LastName, reqBody.Email, "")
if err != nil {
httperror.InternalError(ctx, "Cannot update user profiles", err, nil).Render(rw)
return
Expand All @@ -224,9 +238,9 @@ func (h ProfileHandler) PatchUserProfile(rw http.ResponseWriter, req *http.Reque
func (h ProfileHandler) PatchUserPassword(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

token, ok := ctx.Value(middleware.TokenContextKey).(string)
if !ok {
httperror.Unauthorized("", nil, nil).Render(rw)
token, user, httpErr := getTokenAndUser(ctx, h.AuthManager)
if httpErr != nil {
httpErr.Render(rw)
return
}

Expand Down Expand Up @@ -267,48 +281,22 @@ func (h ProfileHandler) PatchUserPassword(rw http.ResponseWriter, req *http.Requ
return
}

log.Ctx(ctx).Warnf("[PatchUserPassword] - Will update password for user account ID %s", user.ID)
err = h.AuthManager.UpdatePassword(ctx, token, reqBody.CurrentPassword, reqBody.NewPassword)
if err != nil {
httperror.InternalError(ctx, "Cannot update user password", err, nil).Render(rw)
return
}

userID, err := h.AuthManager.GetUserID(ctx, token)
if err != nil {
httperror.InternalError(ctx, "Cannot get user ID", err, nil).Render(rw)
return
}
log.Ctx(ctx).Infof("[UpdateUserPassword] - Updated password for user with account ID %s", userID)

httpjson.RenderStatus(rw, http.StatusOK, map[string]string{"message": "user password updated successfully"}, httpjson.JSON)
}

func (h ProfileHandler) GetProfile(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()

token, ok := ctx.Value(middleware.TokenContextKey).(string)
if !ok {
httperror.Unauthorized("", nil, nil).Render(rw)
return
}

user, err := h.AuthManager.GetUser(ctx, token)
if err != nil {
if errors.Is(err, auth.ErrInvalidToken) {
err = fmt.Errorf("getting user profile: %w", err)
log.Ctx(ctx).Error(err)
httperror.Unauthorized("", err, nil).Render(rw)
return
}

if errors.Is(err, auth.ErrUserNotFound) {
err = fmt.Errorf("user from token %s not found: %w", token, err)
log.Ctx(ctx).Error(err)
httperror.BadRequest("", err, nil).Render(rw)
return
}

httperror.InternalError(ctx, "Cannot get user", err, nil).Render(rw)
_, user, httpErr := getTokenAndUser(ctx, h.AuthManager)
if httpErr != nil {
httpErr.Render(rw)
return
}

Expand Down Expand Up @@ -425,3 +413,29 @@ func (h ProfileHandler) GetOrganizationLogo(rw http.ResponseWriter, req *http.Re
httperror.InternalError(ctx, "Cannot write organization logo to response", err, nil).Render(rw)
}
}

func getTokenAndUser(ctx context.Context, authManager auth.AuthManager) (token string, user *auth.User, httpErr *httperror.HTTPError) {
token, ok := ctx.Value(middleware.TokenContextKey).(string)
if !ok {
return "", nil, httperror.Unauthorized("", nil, nil)
}

user, err := authManager.GetUser(ctx, token)
if err != nil {
if errors.Is(err, auth.ErrInvalidToken) {
err = fmt.Errorf("getting user profile: %w", err)
log.Ctx(ctx).Error(err)
return "", nil, httperror.Unauthorized("", err, nil)
}

if errors.Is(err, auth.ErrUserNotFound) {
err = fmt.Errorf("user from token %s not found: %w", token, err)
log.Ctx(ctx).Error(err)
return "", nil, httperror.BadRequest("", err, nil)
}

return "", nil, httperror.InternalError(ctx, "Cannot get user", err, nil)
}

return token, user, nil
}
Loading

0 comments on commit ce0d6a8

Please sign in to comment.