From c494398437b20a533396a8bce56a1b26f48e3663 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 26 Oct 2023 15:03:11 -0600 Subject: [PATCH 01/10] remove findExistingConfirmation The function was writing HTTP responses and logging, both of which are better done in the actual HTTP handler methods. When these helper methods know about HTTP and can (but don't always) write responses, it makes the handlers awkward to write, because it's never clear whether they should just return afterwards, or if they can do any error recovery or better logging, etc. Without the logging and sometimes writing of HTTP responses, the helper wasn't doing anything, and so it's easily removed. Also, the error handling of FindConfirmation is made consistent as an internal server error. BACK-2500 --- api/clinicianInvites.go | 19 ++++++++----------- api/forgot.go | 5 ++--- api/hydrophoneApi.go | 42 ++++++++++++++++------------------------- api/invite.go | 29 ++++++++++++++-------------- api/patientInvites.go | 14 ++++++-------- api/signup.go | 8 +++----- 6 files changed, 49 insertions(+), 68 deletions(-) diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index 59bc288bb..68d51a164 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -126,10 +126,9 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, Type: models.TypeClinicianInvite, Status: models.StatusPending, } - confirmation, err := a.findExistingConfirmation(req.Context(), filter, res) + confirmation, err := a.Store.FindConfirmation(req.Context(), filter) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if confirmation == nil { @@ -192,10 +191,9 @@ func (a *Api) GetClinicianInvite(res http.ResponseWriter, req *http.Request, var Type: models.TypeClinicianInvite, Status: models.StatusPending, } - confirmation, err := a.findExistingConfirmation(req.Context(), filter, res) + confirmation, err := a.Store.FindConfirmation(req.Context(), filter) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if confirmation == nil { @@ -269,10 +267,9 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, Status: models.StatusPending, } - conf, err := a.findExistingConfirmation(req.Context(), accept, res) + conf, err := a.Store.FindConfirmation(req.Context(), accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } @@ -330,7 +327,7 @@ func (a *Api) DismissClinicianInvite(res http.ResponseWriter, req *http.Request, Type: models.TypeClinicianInvite, Status: models.StatusPending, } - conf, err := a.findExistingConfirmation(ctx, filter, res) + conf, err := a.Store.FindConfirmation(ctx, filter) if err != nil { a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return @@ -361,7 +358,7 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, Type: models.TypeClinicianInvite, Status: models.StatusPending, } - conf, err := a.findExistingConfirmation(ctx, filter, res) + conf, err := a.Store.FindConfirmation(ctx, filter) if err != nil { a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return diff --git a/api/forgot.go b/api/forgot.go index 02e1ac192..b969171b5 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -96,10 +96,9 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map func (a *Api) findResetConfirmation(conf *models.Confirmation, ctx context.Context, res http.ResponseWriter) *models.Confirmation { log.Printf("findResetConfirmation: finding [%v]", conf) - found, err := a.findExistingConfirmation(ctx, conf, res) + found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { - log.Printf("findResetConfirmation: error [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } if found == nil { diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 1050a6ee7..6413b31a7 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -280,18 +280,6 @@ func (a *Api) addOrUpdateConfirmation(ctx context.Context, conf *models.Confirma return true } -// Find this confirmation -// write error if it fails -func (a *Api) findExistingConfirmation(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) (*models.Confirmation, error) { - if found, err := a.Store.FindConfirmation(ctx, conf); err != nil { - log.Printf("findExistingConfirmation: [%v]", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION)} - return nil, statusErr - } else { - return found, nil - } -} - // Find this confirmation // write error if it fails func (a *Api) addProfile(conf *models.Confirmation) error { @@ -511,21 +499,23 @@ func (a *Api) sendModelAsResWithStatus(res http.ResponseWriter, model interface{ } func (a *Api) sendError(res http.ResponseWriter, statusCode int, reason string, extras ...interface{}) { - _, file, line, ok := runtime.Caller(1) - if ok { - segments := strings.Split(file, "/") - file = segments[len(segments)-1] - } else { - file = "???" - line = 0 - } - - messages := make([]string, len(extras)) - for index, extra := range extras { - messages[index] = fmt.Sprintf("%v", extra) + errs := []error{} + nonErrs := []interface{}{} + for _, extra := range extras { + if err, ok := extra.(error); ok { + errs = append(errs, err) + } else { + nonErrs = append(nonErrs, extra) + } } - - log.Printf("%s:%d RESPONSE ERROR: [%d %s] %s", file, line, statusCode, reason, strings.Join(messages, "; ")) + a.logger. + Desugar(). + WithOptions(zap.AddCallerSkip(1)). + Sugar(). + With(zap.Int("code", statusCode)). + With(zap.Errors("errors", errs)). + With("extras", nonErrs). + Error(reason) a.sendModelAsResWithStatus(res, status.NewStatus(statusCode, reason), statusCode) } diff --git a/api/invite.go b/api/invite.go index 4e9f01845..d153f0693 100644 --- a/api/invite.go +++ b/api/invite.go @@ -206,10 +206,9 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ return } - conf, err := a.findExistingConfirmation(req.Context(), accept, res) + conf, err := a.Store.FindConfirmation(req.Context(), accept) if err != nil { - log.Printf("AcceptInvite error while finding confirmation [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { @@ -322,11 +321,12 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ Type: models.TypeCareteamInvite, } - if conf, err := a.findExistingConfirmation(req.Context(), invite, res); err != nil { - log.Printf("CancelInvite: finding [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + conf, err := a.Store.FindConfirmation(req.Context(), invite) + if err != nil { + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return - } else if conf != nil { + } + if conf != nil { //cancel the invite conf.UpdateStatus(models.StatusCanceled) @@ -376,12 +376,12 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map return } - if conf, err := a.findExistingConfirmation(req.Context(), dismiss, res); err != nil { - log.Printf("DismissInvite: finding [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + conf, err := a.Store.FindConfirmation(req.Context(), dismiss) + if err != nil { + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return - } else if conf != nil { - + } + if conf != nil { conf.UpdateStatus(models.StatusDeclined) if a.addOrUpdateConfirmation(req.Context(), conf, res) { @@ -554,10 +554,9 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ Type: models.TypeCareteamInvite, } - invite, err := a.findExistingConfirmation(req.Context(), find, res) + invite, err := a.Store.FindConfirmation(req.Context(), find) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if invite == nil || invite.ClinicId != "" { diff --git a/api/patientInvites.go b/api/patientInvites.go index 458558b0c..6c5f02b21 100644 --- a/api/patientInvites.go +++ b/api/patientInvites.go @@ -66,16 +66,15 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va Key: inviteId, } - conf, err := a.findExistingConfirmation(req.Context(), accept, res) + conf, err := a.Store.FindConfirmation(req.Context(), accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { - a.logger.Warn("confirmation not found") statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.logger.With(zap.Error(statusErr)).Info(statusInviteNotFoundMessage) return } @@ -138,16 +137,15 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re Key: inviteId, } - conf, err := a.findExistingConfirmation(req.Context(), accept, res) + conf, err := a.Store.FindConfirmation(req.Context(), accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { - a.logger.Warn("confirmation not found") statusErr := &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)} a.sendModelAsResWithStatus(res, statusErr, http.StatusForbidden) + a.logger.With(zap.Error(statusErr)).Info(statusInviteNotFoundMessage) return } diff --git a/api/signup.go b/api/signup.go index 7f92a19fe..0e0434353 100644 --- a/api/signup.go +++ b/api/signup.go @@ -47,10 +47,9 @@ const ( // try to find the signup confirmation func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) *models.Confirmation { - found, err := a.findExistingConfirmation(ctx, conf, res) + found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { - log.Printf("findSignUp: error [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } if found == nil { @@ -80,13 +79,12 @@ func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.Respons return } - found, err := a.findExistingConfirmation(req.Context(), fromBody, res) + found, err := a.Store.FindConfirmation(req.Context(), fromBody) if err != nil { a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if found != nil { - updatedStatus := string(newStatus) + " signup" log.Printf("updateSignupConfirmation: %s", updatedStatus) found.UpdateStatus(newStatus) From 678fb097b2e8bbc8c11bd105d225d3ce1154dd58 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 26 Oct 2023 15:13:52 -0600 Subject: [PATCH 02/10] rework findResetConfirmation The function was writing HTTP responses and logging, both of which are better done in the actual HTTP handler methods. When these helper methods know about HTTP and can (but don't always) write responses, it makes the handlers awkward to write, because it's never clear whether they should just return afterwards, or if they can do any error recovery or better logging, etc. As one example, findResetConfirmation would write 404 responses if the confirmation wasn't found. The handler ignored that one particular case, and it wasn't obvious why it was ignoring it. Even as the handler ignored it, it still had to have a nil check/guard statement, which indented the code awkwardly and left the reader wondering just what was returned the user if conf was nil. BACK-2500 --- api/forgot.go | 90 ++++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/api/forgot.go b/api/forgot.go index b969171b5..6a278ec1c 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -6,6 +6,8 @@ import ( "log" "net/http" + "go.uber.org/zap" + "github.com/tidepool-org/go-common/clients/shoreline" "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" @@ -93,28 +95,20 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map } // find the reset confirmation if it exists and hasn't expired -func (a *Api) findResetConfirmation(conf *models.Confirmation, ctx context.Context, res http.ResponseWriter) *models.Confirmation { - - log.Printf("findResetConfirmation: finding [%v]", conf) +func (a *Api) findResetConfirmation(ctx context.Context, conf *models.Confirmation) (*models.Confirmation, bool, error) { + a.logger.With("conf", conf).Debugf("finding reset confirmation") found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) - return nil + return nil, false, err } if found == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_RESET_NOT_FOUND)} - log.Printf("findResetConfirmation: not found [%s]\n", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - return nil + return nil, false, nil } if found.IsExpired() { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_RESET_EXPIRED)} - log.Printf("findResetConfirmation: expired [%s]\n", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - return nil + return nil, true, nil } - return found + return found, false, nil } // Accept the password change @@ -144,34 +138,50 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma resetCnf := &models.Confirmation{Key: rb.Key, Email: rb.Email, Status: models.StatusPending, Type: models.TypePasswordReset} - if conf := a.findResetConfirmation(resetCnf, req.Context(), res); conf != nil { - if resetCnf.Key == "" || resetCnf.Email != conf.Email { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + conf, expired, err := a.findResetConfirmation(req.Context(), resetCnf) + if err != nil { + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + return + } + if expired { + statusErr := &status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_RESET_EXPIRED)} + a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.logger.With(zap.Error(statusErr)).Info(STATUS_RESET_EXPIRED) + return + } + if conf == nil { + statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_RESET_NOT_FOUND)} + a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.logger.With(zap.Error(statusErr)).Info(STATUS_RESET_NOT_FOUND) + return + } + + if resetCnf.Key == "" || resetCnf.Email != conf.Email { + statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} + a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + return + } + + token := a.sl.TokenProvide() + + if usr := a.findExistingUser(rb.Email, token); usr != nil { + + if err := a.sl.UpdateUser(usr.UserID, shoreline.UserUpdate{Password: &rb.Password}, token); err != nil { + status := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} + a.sendModelAsResWithStatus(res, status, http.StatusBadRequest) + a.logger.With(zap.Error(err)).Info("updating user password") return } - - token := a.sl.TokenProvide() - - if usr := a.findExistingUser(rb.Email, token); usr != nil { - - if err := a.sl.UpdateUser(usr.UserID, shoreline.UserUpdate{Password: &rb.Password}, token); err != nil { - log.Printf("acceptPassword: error updating password as part of password reset [%v]", err) - status := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} - a.sendModelAsResWithStatus(res, status, http.StatusBadRequest) - return - } - conf.UpdateStatus(models.StatusCompleted) - if a.addOrUpdateConfirmation(req.Context(), conf, res) { - //STATUS_RESET_ACCEPTED - a.logMetricAsServer("password reset") - a.sendModelAsResWithStatus( - res, - status.StatusError{Status: status.NewStatus(http.StatusOK, STATUS_RESET_ACCEPTED)}, - http.StatusOK, - ) - return - } + conf.UpdateStatus(models.StatusCompleted) + if a.addOrUpdateConfirmation(req.Context(), conf, res) { + //STATUS_RESET_ACCEPTED + a.logMetricAsServer("password reset") + a.sendModelAsResWithStatus( + res, + status.StatusError{Status: status.NewStatus(http.StatusOK, STATUS_RESET_ACCEPTED)}, + http.StatusOK, + ) + return } } } From d920f2dacdd2f318738e3d96f3f0cd3874b5305e Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Wed, 25 Oct 2023 17:07:19 -0600 Subject: [PATCH 03/10] move to zap logging During code review, it was requested that the zap logger be used. I might have gone a bit overboard, but I've moved everything over to zap. I think it's still TBD if zap is what we really want, but at least now we can see it in action, and better evaluate if it brings advantages over other options. Zap produces a stack trace at the Error log level, so I used that as a guide. In general, any time a function returned an error, and that error caused the HTTP handler to abort its processing with a 5XX HTTP status, I used the Error level for the log, so we'll log the stack trace that led up to the error. In the event that an error was returned and the handler returned a 4XX HTTP status, I used the Info log level. My thinking was if the handler was telling the client that the error was on their side, then whatever the Go error was, we didn't need a stack trace. This happens a lot for example when a request body contains invalid JSON. The Unmarshal call will fail with an error and the handler returns a HTTP 400 or similar. A stack trace in that event wouldn't be useful. In general, I didn't use the Warn level much, as it behaves differently in prod versus non-prod. In the former, it doesn't include a stack trace, while in the latter it does. I used it during an optional permissions check. In development, we'll get better debug info, but in production we'll skip the stack trace, as the error isn't going to cause an abort of the handler method. I also refactored many API responses to use the existing sendError and other related methods. This should help maintain consistency in the future, and should we decide to remove zap for example, we'll have fewer places to modify, as the move to using sendError and friends has greatly reduced the number of log calls that were scattered around the code base. (Last I checked, the number of logging sites was reduced from 153 to 49, according to rough grep checks.) Another request from code review, was that logging should include the userId where one existed. This was accomplished using a middleware function, such that the userId is added to the logger's context and will automatically appear in the log output. There's no need to manually specify it at the logging call site. The zap dependency version was updated to 1.22 to get access to the WithOptions method of the Sugared logger, which makes some of our logging cleaner. BACK-2500 --- api/clinic.go | 22 +- api/clinicianInvites.go | 82 +++---- api/forgot.go | 34 +-- api/forgot_test.go | 15 +- api/hydrophoneApi.go | 353 ++++++++++++++++++------------- api/hydrophoneApi_test.go | 19 +- api/invite.go | 165 ++++++--------- api/invite_test.go | 55 ++--- api/patientInvites.go | 59 ++---- api/signup.go | 151 +++++-------- api/signup_test.go | 14 +- clients/mockNotifier.go | 4 +- clients/mongoStoreClient.go | 15 +- clients/mongoStoreClient_test.go | 3 +- clients/sesNotifier.go | 30 +-- events/events.go | 2 - go.work.sum | 5 + hydrophone.go | 16 +- models/confirmation.go | 3 - testutil/testutil.go | 17 ++ 20 files changed, 481 insertions(+), 583 deletions(-) create mode 100644 testutil/testutil.go diff --git a/api/clinic.go b/api/clinic.go index eafae11d4..db98fa6b6 100644 --- a/api/clinic.go +++ b/api/clinic.go @@ -9,7 +9,6 @@ import ( clinics "github.com/tidepool-org/clinic/client" commonClients "github.com/tidepool-org/go-common/clients" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) @@ -38,7 +37,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ } if permissions, err := a.tokenUserHasRequestedPermissions(token, inviterID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -48,9 +47,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ defer req.Body.Close() var ib = &ClinicInvite{} if err := json.NewDecoder(req.Body).Decode(ib); err != nil { - a.logger.Errorw("error decoding invite", zap.Error(err)) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -79,14 +76,11 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ patientExists, err := a.checkExistingPatientOfClinic(ctx, clinicId, inviterID) if err != nil { - a.logger.Errorw("error checking if user is already a patient of clinic", zap.Error(err)) - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } if patientExists { - a.logger.Info("user is already a patient of clinic") - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusConflict, statusExistingPatientMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict) + a.sendError(res, http.StatusConflict, statusExistingPatientMessage) return } existingInvite, err := a.checkForDuplicateClinicInvite(req.Context(), clinicId, inviterID) @@ -95,10 +89,8 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ return } if existingInvite { - a.logger.Infof("clinic %s user already has or had an invite from %v", clinicId, inviterID) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusConflict, statusExistingInviteMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict) - a.logger.With(zap.Error(statusErr)).Error("finding confirmation") + a.sendError(res, http.StatusConflict, statusExistingInviteMessage, + zap.String("clinicId", clinicId), zap.String("inviterID", inviterID), err) return } @@ -138,7 +130,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ a.logMetric("invite created", req) if err := a.addProfile(invite); err != nil { - a.logger.Errorw("error adding profile information to confirmation", zap.Error(err)) + a.logger.With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) return } else if !suppressEmail { fullName := invite.Creator.Profile.FullName diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index 68d51a164..990a15a31 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -7,11 +7,8 @@ import ( "net/http" "time" - "go.uber.org/zap" - clinics "github.com/tidepool-org/clinic/client" "github.com/tidepool-org/go-common/clients/shoreline" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) @@ -27,7 +24,6 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va clinicId := vars["clinicId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { - a.logger.Warnw("token owner is not clinic admin", err) return } @@ -40,9 +36,7 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va defer req.Body.Close() var body = &ClinicianInvite{} if err := json.NewDecoder(req.Body).Decode(body); err != nil { - a.logger.Errorw("error decoding invite", zap.Error(err)) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -78,9 +72,9 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va return } - statusErr := a.sendClinicianConfirmation(req, confirmation) - if statusErr != nil { - a.sendError(res, statusErr.Code, statusErr.Reason, statusErr.Error()) + code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) + if code != 0 { + a.sendError(res, code, msg, optionalErr) return } @@ -99,7 +93,6 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { - a.logger.Warnw("token owner is not clinic admin", err) return } @@ -150,9 +143,9 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, confirmation.UserId = invitedUsr.UserID } - statusErr := a.sendClinicianConfirmation(req, confirmation) - if statusErr != nil { - a.sendError(res, statusErr.Code, statusErr.Reason, statusErr.Error()) + code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) + if code > 0 { + a.sendError(res, code, msg, optionalErr) return } @@ -169,7 +162,6 @@ func (a *Api) GetClinicianInvite(res http.ResponseWriter, req *http.Request, var inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { - a.logger.Warnw("token owner is not clinic admin", err) return } @@ -216,29 +208,29 @@ func (a *Api) GetClinicianInvitations(res http.ResponseWriter, req *http.Request // Tokens only legit when for same userid if userId != token.UserID || invitedUsr == nil || invitedUsr.UserID == "" { - a.logger.Errorw("token belongs to a different user or user doesn't exist") - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeClinicianInvite}, models.StatusPending) if err != nil { - a.logger.Errorw("error retrieving invites for user", "userId", userId, "error", zap.Error(err)) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION)}, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } - - if invites := a.checkFoundConfirmations(res, found, err); invites != nil { + if len(found) == 0 { + a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + if invites := a.addProfileInfoToConfirmations(found); invites != nil { a.ensureIdSet(req.Context(), userId, invites) if err := a.populateRestrictions(ctx, *invitedUsr, *token, invites); err != nil { - a.logger.Errorw("error populating restriction in invites for user", "userId", userId, "error", zap.Error(err)) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION)}, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } - a.logger.Infof("found and checked %v invites", len(invites)) a.logMetric("get_clinician_invitations", req) a.sendModelAsResWithStatus(res, invites, http.StatusOK) + a.logger.Infof("invites found and checked: %d", len(invites)) return } } @@ -255,8 +247,7 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.logger.Warnw("token belongs to a different user or user doesn't exist") - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } @@ -274,13 +265,12 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, } if err := a.populateRestrictions(ctx, *invitedUsr, *token, []*models.Confirmation{conf}); err != nil { - a.logger.Errorw("error populating restriction in invites for user", "userId", userId, "error", zap.Error(err)) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION)}, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf.Restrictions != nil && !conf.Restrictions.CanAccept { - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusForbidden, STATUS_ERR_ACCEPTING_CONFIRMATION)}, http.StatusForbidden) + a.sendError(res, http.StatusForbidden, STATUS_ERR_ACCEPTING_CONFIRMATION) return } @@ -293,9 +283,7 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, conf.UpdateStatus(models.StatusCompleted) if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.logger.Errorw("error while adding or updating confirmation", zap.Error(err)) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } @@ -316,8 +304,7 @@ func (a *Api) DismissClinicianInvite(res http.ResponseWriter, req *http.Request, invitedUsr := a.findExistingUser(token.UserID, req.Header.Get(TP_SESSION_TOKEN)) // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.logger.Warnw("token belongs to a different user or user doesn't exist") - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } @@ -348,7 +335,6 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { - a.logger.Warnw("token owner is not clinic admin", err) return } @@ -368,18 +354,16 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, } } -func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) *status.StatusError { +func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) (code int, msg string, err error) { ctx := req.Context() if err := a.addProfile(confirmation); err != nil { - a.logger.Errorw("error adding profile information to confirmation", zap.Error(err)) - return &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} + return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err } confirmation.Modified = time.Now() if err := a.Store.UpsertConfirmation(ctx, confirmation); err != nil { - a.logger.Errorw("error upserting clinician confirmation confirmation", zap.Error(err)) - return &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} + return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err } a.logMetric("clinician_invite_created", req) @@ -399,13 +383,11 @@ func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models. } if !a.createAndSendNotification(req, confirmation, emailContent) { - return &status.StatusError{ - Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SENDING_EMAIL), - } + return http.StatusInternalServerError, STATUS_ERR_SENDING_EMAIL, nil } a.logMetric("clinician_invite_sent", req) - return nil + return 0, "", nil } func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http.Request, filter, conf *models.Confirmation, statusUpdate models.Status) { @@ -413,17 +395,13 @@ func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http response, err := a.clinics.DeleteInvitedClinicianWithResponse(ctx, clinics.ClinicId(filter.ClinicId), clinics.InviteId(filter.Key)) if err != nil || (response.StatusCode() != http.StatusOK && response.StatusCode() != http.StatusNotFound) { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { conf.UpdateStatus(statusUpdate) if !a.addOrUpdateConfirmation(ctx, conf, res) { - a.logger.Warn("error adding or updating confirmation") - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) return } } @@ -441,7 +419,7 @@ func (a *Api) assertClinicMember(ctx context.Context, clinicId string, token *sh a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return err } else if result.StatusCode() != http.StatusOK { - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("unexpected status code %v when fetching clinician %v from clinic %v", result.StatusCode(), token.UserID, clinicId) } } @@ -455,7 +433,7 @@ func (a *Api) assertClinicAdmin(ctx context.Context, clinicId string, token *sho a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return err } else if result.StatusCode() != http.StatusOK { - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("unexpected status code %v when fetching clinician %v from clinic %v", result.StatusCode(), token.UserID, clinicId) } else { clinician := result.JSON200 @@ -464,7 +442,7 @@ func (a *Api) assertClinicAdmin(ctx context.Context, clinicId string, token *sho return nil } } - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("the clinician doesn't have the required permissions %v", clinician.Roles) } } diff --git a/api/forgot.go b/api/forgot.go index 6a278ec1c..6ba2fc783 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -3,13 +3,11 @@ package api import ( "context" "encoding/json" - "log" "net/http" "go.uber.org/zap" "github.com/tidepool-org/go-common/clients/shoreline" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) @@ -62,8 +60,6 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map if resetUsr := a.findExistingUser(resetCnf.Email, a.sl.TokenProvide()); resetUsr != nil { resetCnf.UserId = resetUsr.UserID } else { - log.Print(STATUS_RESET_NO_ACCOUNT) - log.Printf("email used [%s]", email) resetCnf, err = models.NewConfirmation(models.TypeNoAccount, models.TemplateNameNoAccount, "") if err != nil { a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) @@ -73,6 +69,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map resetCnf.Email = email //there is nothing more to do other than notify the user resetCnf.UpdateStatus(models.StatusCompleted) + a.logger.With(zap.String("email", email)).Info(STATUS_RESET_NO_ACCOUNT) } if a.addOrUpdateConfirmation(req.Context(), resetCnf, res) { @@ -87,7 +84,6 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map a.logMetricAsServer("reset confirmation sent") } else { a.logMetricAsServer("reset confirmation failed to be sent") - log.Print("Something happened generating a passwordReset email") } } //unless no email was given we say its all good @@ -96,7 +92,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map // find the reset confirmation if it exists and hasn't expired func (a *Api) findResetConfirmation(ctx context.Context, conf *models.Confirmation) (*models.Confirmation, bool, error) { - a.logger.With("conf", conf).Debugf("finding reset confirmation") + a.logger.With("conf", conf).Debug("finding reset confirmation") found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { return nil, false, err @@ -130,9 +126,7 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma defer req.Body.Close() var rb = &resetBody{} if err := json.NewDecoder(req.Body).Decode(rb); err != nil { - log.Printf("acceptPassword: error decoding reset details %v\n", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -144,21 +138,16 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma return } if expired { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_RESET_EXPIRED)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - a.logger.With(zap.Error(statusErr)).Info(STATUS_RESET_EXPIRED) + a.sendError(res, http.StatusNotFound, STATUS_RESET_EXPIRED) return } if conf == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_RESET_NOT_FOUND)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - a.logger.With(zap.Error(statusErr)).Info(STATUS_RESET_NOT_FOUND) + a.sendError(res, http.StatusNotFound, STATUS_RESET_NOT_FOUND) return } if resetCnf.Key == "" || resetCnf.Email != conf.Email { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_RESET_ERROR) return } @@ -167,20 +156,13 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma if usr := a.findExistingUser(rb.Email, token); usr != nil { if err := a.sl.UpdateUser(usr.UserID, shoreline.UserUpdate{Password: &rb.Password}, token); err != nil { - status := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_RESET_ERROR)} - a.sendModelAsResWithStatus(res, status, http.StatusBadRequest) - a.logger.With(zap.Error(err)).Info("updating user password") + a.sendError(res, http.StatusBadRequest, STATUS_RESET_ERROR, err, "updating user password") return } conf.UpdateStatus(models.StatusCompleted) if a.addOrUpdateConfirmation(req.Context(), conf, res) { - //STATUS_RESET_ACCEPTED a.logMetricAsServer("password reset") - a.sendModelAsResWithStatus( - res, - status.StatusError{Status: status.NewStatus(http.StatusOK, STATUS_RESET_ACCEPTED)}, - http.StatusOK, - ) + a.sendOK(res, STATUS_RESET_ACCEPTED) return } } diff --git a/api/forgot_test.go b/api/forgot_test.go index a71d5feab..f2c7d09f7 100644 --- a/api/forgot_test.go +++ b/api/forgot_test.go @@ -6,9 +6,9 @@ import ( "net/http/httptest" "testing" - "go.uber.org/zap" - "github.com/gorilla/mux" + + "github.com/tidepool-org/hydrophone/testutil" ) func TestForgotResponds(t *testing.T) { @@ -57,19 +57,20 @@ func TestForgotResponds(t *testing.T) { }, } + logger := testutil.NewLogger(t) for idx, test := range tests { //fresh each time var testRtr = mux.NewRouter() + store := mockStore if test.returnNone { - hydrophoneFindsNothing := NewApi(FAKE_CONFIG, nil, mockStoreEmpty, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, zap.NewNop().Sugar()) - hydrophoneFindsNothing.SetHandlers("", testRtr) - } else { - hydrophone := NewApi(FAKE_CONFIG, nil, mockStore, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, zap.NewNop().Sugar()) - hydrophone.SetHandlers("", testRtr) + store = mockStoreEmpty } + hydrophone := NewApi(FAKE_CONFIG, nil, store, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, logger) + hydrophone.SetHandlers("", testRtr) + var body = &bytes.Buffer{} // build the body only if there is one defined in the test if len(test.body) != 0 { diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 6413b31a7..96c85dec3 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -4,16 +4,15 @@ import ( "context" "encoding/json" "fmt" - "log" "net/http" "reflect" - "runtime" "strings" "github.com/gorilla/mux" "github.com/kelseyhightower/envconfig" "go.uber.org/fx" "go.uber.org/zap" + "go.uber.org/zap/zapcore" clinicsClient "github.com/tidepool-org/clinic/client" commonClients "github.com/tidepool-org/go-common/clients" @@ -58,27 +57,50 @@ type AlertsClient interface { const ( TP_SESSION_TOKEN = "x-tidepool-session-token" - //returned error messages - STATUS_ERR_SENDING_EMAIL = "Error sending email" - STATUS_ERR_SAVING_CONFIRMATION = "Error saving the confirmation" - STATUS_ERR_CREATING_CONFIRMATION = "Error creating a confirmation" - STATUS_ERR_FINDING_CONFIRMATION = "Error finding the confirmation" STATUS_ERR_ACCEPTING_CONFIRMATION = "Error accepting invitation" - STATUS_ERR_FINDING_USER = "Error finding the user" - STATUS_ERR_FINDING_CLINIC = "Error finding the clinic" + STATUS_ERR_ADDING_PROFILE = "Error adding profile" + STATUS_ERR_CREATING_ALERTS_CONFIG = "Error creating alerts configuration" + STATUS_ERR_CREATING_CONFIRMATION = "Error creating a confirmation" + STATUS_ERR_CREATING_PATIENT = "Error creating patient" STATUS_ERR_DECODING_CONFIRMATION = "Error decoding the confirmation" STATUS_ERR_DECODING_CONTEXT = "Error decoding the confirmation context" - STATUS_ERR_VALIDATING_CONTEXT = "Error validating the confirmation context" - STATUS_ERR_CREATING_PATIENT = "Error creating patient" + STATUS_ERR_DELETING_CONFIRMATION = "Error deleting a confirmation" + STATUS_ERR_FINDING_CLINIC = "Error finding the clinic" + STATUS_ERR_FINDING_CONFIRMATION = "Error finding the confirmation" STATUS_ERR_FINDING_PREVIEW = "Error finding the invite preview" - STATUS_ERR_CREATING_ALERTS_CONFIG = "Error creating alerts configuration" + STATUS_ERR_FINDING_USER = "Error finding the user" + STATUS_ERR_RESETTING_KEY = "Error resetting key" + STATUS_ERR_SAVING_CONFIRMATION = "Error saving the confirmation" + STATUS_ERR_SENDING_EMAIL = "Error sending email" + STATUS_ERR_SETTING_PERMISSIONS = "Error setting permissions" + STATUS_ERR_UPDATING_USER = "Error updating user" + STATUS_ERR_VALIDATING_CONTEXT = "Error validating the confirmation context" - //returned status messages - STATUS_NOT_FOUND = "Nothing found" - STATUS_NO_TOKEN = "No x-tidepool-session-token was found" - STATUS_INVALID_TOKEN = "The x-tidepool-session-token was invalid" - STATUS_UNAUTHORIZED = "Not authorized for requested operation" - STATUS_OK = "OK" + STATUS_EXISTING_SIGNUP = "User already has an existing valid signup confirmation" + STATUS_INVALID_BIRTHDAY = "Birthday specified is invalid" + STATUS_INVALID_PASSWORD = "Password specified is invalid" + STATUS_INVALID_TOKEN = "The x-tidepool-session-token was invalid" + STATUS_MISMATCH_BIRTHDAY = "Birthday specified does not match patient birthday" + STATUS_MISSING_BIRTHDAY = "Birthday is missing" + STATUS_MISSING_PASSWORD = "Password is missing" + STATUS_NO_PASSWORD = "User does not have a password" + STATUS_NO_TOKEN = "No x-tidepool-session-token was found" + STATUS_OK = "OK" + STATUS_SIGNUP_ACCEPTED = "User has had signup confirmed" + STATUS_SIGNUP_ERROR = "Error while completing signup confirmation. The signup confirmation remains active until it expires" + STATUS_SIGNUP_EXPIRED = "The signup confirmation has expired" + STATUS_SIGNUP_NOT_FOUND = "No matching signup confirmation was found" + STATUS_SIGNUP_NO_CONF = "Required confirmation id is missing" + STATUS_SIGNUP_NO_ID = "Required userid is missing" + STATUS_UNAUTHORIZED = "Not authorized for requested operation" + STATUS_NOT_FOUND = "Nothing found" + + ERROR_NO_PASSWORD = 1001 + ERROR_MISSING_PASSWORD = 1002 + ERROR_INVALID_PASSWORD = 1003 + ERROR_MISSING_BIRTHDAY = 1004 + ERROR_INVALID_BIRTHDAY = 1005 + ERROR_MISMATCH_BIRTHDAY = 1006 ) func NewApi( @@ -135,8 +157,31 @@ func routerProvider(api *Api) *mux.Router { // RouterModule build a router var RouterModule = fx.Options(fx.Provide(routerProvider, apiConfigProvider)) -func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { +// addUserIDToLogger adds userID to the logging context. +// +// It uses the first matching userID it finds, additional userIDs (which +// shouldn't exist anyway) are ignored. +// +// This is effected via its type being that of a mux.MiddlewareFunc. +func (a *Api) addUserIDToLogger(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + for key := range vars { + if !strings.EqualFold(key, "userid") { + continue + } + oldLogger := a.logger + a.logger = a.logger.With(key, vars[key]) + defer func(l *zap.SugaredLogger) { + a.logger = l + }(oldLogger) + break + } + h.ServeHTTP(w, r) + }) +} +func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { c := rtr.PathPrefix("/confirm").Subrouter() c.HandleFunc("/status", a.IsReady).Methods("GET") @@ -148,103 +193,108 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { c.HandleFunc("/live", a.IsAlive).Methods("GET") rtr.HandleFunc("/live", a.IsAlive).Methods("GET") + // uid is a shortened name for this middleware + uid := a.addUserIDToLogger + // vars is a shorthand for applying the varsHandler to an handler. + type vars = varsHandler + // POST /confirm/send/signup/:userid // POST /confirm/send/forgot/:useremail // POST /confirm/send/invite/:userid csend := rtr.PathPrefix("/confirm/send").Subrouter() - csend.Handle("/signup/{userid}", varsHandler(a.sendSignUp)).Methods("POST") - csend.Handle("/forgot/{useremail}", varsHandler(a.passwordReset)).Methods("POST") - csend.Handle("/invite/{userid}", varsHandler(a.SendInvite)).Methods("POST") - csend.Handle("/invite/{userId}/clinic", varsHandler(a.InviteClinic)).Methods("POST") + csend.Handle("/signup/{userid}", uid(vars(a.sendSignUp))).Methods("POST") + csend.Handle("/forgot/{useremail}", vars(a.passwordReset)).Methods("POST") + csend.Handle("/invite/{userid}", uid(vars(a.SendInvite))).Methods("POST") + csend.Handle("/invite/{userId}/clinic", uid(vars(a.InviteClinic))).Methods("POST") send := rtr.PathPrefix("/send").Subrouter() - send.Handle("/signup/{userid}", varsHandler(a.sendSignUp)).Methods("POST") - send.Handle("/forgot/{useremail}", varsHandler(a.passwordReset)).Methods("POST") - send.Handle("/invite/{userid}", varsHandler(a.SendInvite)).Methods("POST") - send.Handle("/invite/{userId}/clinic", varsHandler(a.InviteClinic)).Methods("POST") + send.Handle("/signup/{userid}", uid(vars(a.sendSignUp))).Methods("POST") + send.Handle("/forgot/{useremail}", vars(a.passwordReset)).Methods("POST") + send.Handle("/invite/{userid}", uid(vars(a.SendInvite))).Methods("POST") + send.Handle("/invite/{userId}/clinic", uid(vars(a.InviteClinic))).Methods("POST") // POST /confirm/resend/signup/:useremail // POST /confirm/resend/invite/:inviteId - c.Handle("/resend/signup/{useremail}", varsHandler(a.resendSignUp)).Methods("POST") - c.Handle("/resend/invite/{inviteId}", varsHandler(a.ResendInvite)).Methods("PATCH") + c.Handle("/resend/signup/{useremail}", vars(a.resendSignUp)).Methods("POST") + c.Handle("/resend/invite/{inviteId}", uid(vars(a.ResendInvite))).Methods("PATCH") - rtr.Handle("/resend/signup/{useremail}", varsHandler(a.resendSignUp)).Methods("POST") - rtr.Handle("/resend/invite/{inviteId}", varsHandler(a.ResendInvite)).Methods("PATCH") + rtr.Handle("/resend/signup/{useremail}", vars(a.resendSignUp)).Methods("POST") + rtr.Handle("/resend/invite/{inviteId}", uid(vars(a.ResendInvite))).Methods("PATCH") // PUT /confirm/accept/signup/:confirmationID // PUT /confirm/accept/forgot/ // PUT /confirm/accept/invite/:userid/:invited_by caccept := rtr.PathPrefix("/confirm/accept").Subrouter() - caccept.Handle("/signup/{confirmationid}", varsHandler(a.acceptSignUp)).Methods("PUT") - caccept.Handle("/forgot", varsHandler(a.acceptPassword)).Methods("PUT") - caccept.Handle("/invite/{userid}/{invitedby}", varsHandler(a.AcceptInvite)).Methods("PUT") + caccept.Handle("/signup/{confirmationid}", vars(a.acceptSignUp)).Methods("PUT") + caccept.Handle("/forgot", vars(a.acceptPassword)).Methods("PUT") + caccept.Handle("/invite/{userid}/{invitedby}", uid(vars(a.AcceptInvite))).Methods("PUT") accept := rtr.PathPrefix("/accept").Subrouter() - accept.Handle("/signup/{confirmationid}", varsHandler(a.acceptSignUp)).Methods("PUT") - accept.Handle("/forgot", varsHandler(a.acceptPassword)).Methods("PUT") - accept.Handle("/invite/{userid}/{invitedby}", varsHandler(a.AcceptInvite)).Methods("PUT") + accept.Handle("/signup/{confirmationid}", vars(a.acceptSignUp)).Methods("PUT") + accept.Handle("/forgot", vars(a.acceptPassword)).Methods("PUT") + accept.Handle("/invite/{userid}/{invitedby}", uid(vars(a.AcceptInvite))).Methods("PUT") // GET /confirm/signup/:userid // GET /confirm/invite/:userid - c.Handle("/signup/{userid}", varsHandler(a.getSignUp)).Methods("GET") - c.Handle("/invite/{userid}", varsHandler(a.GetSentInvitations)).Methods("GET") + c.Handle("/signup/{userid}", uid(vars(a.getSignUp))).Methods("GET") + c.Handle("/invite/{userid}", uid(vars(a.GetSentInvitations))).Methods("GET") - rtr.Handle("/signup/{userid}", varsHandler(a.getSignUp)).Methods("GET") - rtr.Handle("/invite/{userid}", varsHandler(a.GetSentInvitations)).Methods("GET") + rtr.Handle("/signup/{userid}", uid(vars(a.getSignUp))).Methods("GET") + rtr.Handle("/invite/{userid}", uid(vars(a.GetSentInvitations))).Methods("GET") // GET /confirm/invitations/:userid - c.Handle("/invitations/{userid}", varsHandler(a.GetReceivedInvitations)).Methods("GET") + c.Handle("/invitations/{userid}", uid(vars(a.GetReceivedInvitations))).Methods("GET") - rtr.Handle("/invitations/{userid}", varsHandler(a.GetReceivedInvitations)).Methods("GET") + rtr.Handle("/invitations/{userid}", uid(vars(a.GetReceivedInvitations))).Methods("GET") // PUT /confirm/dismiss/invite/:userid/:invited_by // PUT /confirm/dismiss/signup/:userid cdismiss := rtr.PathPrefix("/confirm/dismiss").Subrouter() - cdismiss.Handle("/invite/{userid}/{invitedby}", varsHandler(a.DismissInvite)).Methods("PUT") - cdismiss.Handle("/signup/{userid}", varsHandler(a.dismissSignUp)).Methods("PUT") + cdismiss.Handle("/invite/{userid}/{invitedby}", uid(vars(a.DismissInvite))).Methods("PUT") + cdismiss.Handle("/signup/{userid}", uid(vars(a.dismissSignUp))).Methods("PUT") dismiss := rtr.PathPrefix("/dismiss").Subrouter() - dismiss.Handle("/invite/{userid}/{invitedby}", varsHandler(a.DismissInvite)).Methods("PUT") - dismiss.Handle("/signup/{userid}", varsHandler(a.dismissSignUp)).Methods("PUT") + dismiss.Handle("/invite/{userid}/{invitedby}", uid(vars(a.DismissInvite))).Methods("PUT") + dismiss.Handle("/signup/{userid}", uid(vars(a.dismissSignUp))).Methods("PUT") // POST /confirm/signup/:userid - c.Handle("/signup/{userid}", varsHandler(a.createSignUp)).Methods("POST") + c.Handle("/signup/{userid}", uid(vars(a.createSignUp))).Methods("POST") // PUT /confirm/:userid/invited/:invited_address // PUT /confirm/signup/:userid - c.Handle("/{userid}/invited/{invited_address}", varsHandler(a.CancelInvite)).Methods("PUT") - c.Handle("/signup/{userid}", varsHandler(a.cancelSignUp)).Methods("PUT") + c.Handle("/{userid}/invited/{invited_address}", uid(vars(a.CancelInvite))).Methods("PUT") + c.Handle("/signup/{userid}", uid(vars(a.cancelSignUp))).Methods("PUT") - rtr.Handle("/{userid}/invited/{invited_address}", varsHandler(a.CancelInvite)).Methods("PUT") - rtr.Handle("/signup/{userid}", varsHandler(a.cancelSignUp)).Methods("PUT") + rtr.Handle("/{userid}/invited/{invited_address}", uid(vars(a.CancelInvite))).Methods("PUT") + rtr.Handle("/signup/{userid}", uid(vars(a.cancelSignUp))).Methods("PUT") // GET /v1/clinics/:clinicId/invites/patients // GET /v1/clinics/:clinicId/invites/patients/:inviteId - c.Handle("/v1/clinics/{clinicId}/invites/patients", varsHandler(a.GetPatientInvites)).Methods("GET") - c.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", varsHandler(a.AcceptPatientInvite)).Methods("PUT") - c.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", varsHandler(a.CancelOrDismissPatientInvite)).Methods("DELETE") - - rtr.Handle("/v1/clinics/{clinicId}/invites/patients", varsHandler(a.GetPatientInvites)).Methods("GET") - rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", varsHandler(a.AcceptPatientInvite)).Methods("PUT") - rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", varsHandler(a.CancelOrDismissPatientInvite)).Methods("DELETE") - - c.Handle("/v1/clinicians/{userId}/invites", varsHandler(a.GetClinicianInvitations)).Methods("GET") - c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", varsHandler(a.AcceptClinicianInvite)).Methods("PUT") - c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", varsHandler(a.DismissClinicianInvite)).Methods("DELETE") - - rtr.Handle("/v1/clinicians/{userId}/invites", varsHandler(a.GetClinicianInvitations)).Methods("GET") - rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", varsHandler(a.AcceptClinicianInvite)).Methods("PUT") - rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", varsHandler(a.DismissClinicianInvite)).Methods("DELETE") - - c.Handle("/v1/clinics/{clinicId}/invites/clinicians", varsHandler(a.SendClinicianInvite)).Methods("POST") - c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.ResendClinicianInvite)).Methods("PATCH") - c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.GetClinicianInvite)).Methods("GET") - c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.CancelClinicianInvite)).Methods("DELETE") - - rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians", varsHandler(a.SendClinicianInvite)).Methods("POST") - rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.GetClinicianInvite)).Methods("GET") - rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.ResendClinicianInvite)).Methods("PATCH") - rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", varsHandler(a.CancelClinicianInvite)).Methods("DELETE") + c.Handle("/v1/clinics/{clinicId}/invites/patients", vars(a.GetPatientInvites)).Methods("GET") + c.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.AcceptPatientInvite)).Methods("PUT") + c.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.CancelOrDismissPatientInvite)).Methods("DELETE") + + rtr.Handle("/v1/clinics/{clinicId}/invites/patients", vars(a.GetPatientInvites)).Methods("GET") + rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.AcceptPatientInvite)).Methods("PUT") + rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.CancelOrDismissPatientInvite)).Methods("DELETE") + + c.Handle("/v1/clinicians/{userId}/invites", uid(vars(a.GetClinicianInvitations))).Methods("GET") + c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.AcceptClinicianInvite))).Methods("PUT") + c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.DismissClinicianInvite))).Methods("DELETE") + + rtr.Handle("/v1/clinicians/{userId}/invites", uid(vars(a.GetClinicianInvitations))).Methods("GET") + rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.AcceptClinicianInvite))).Methods("PUT") + rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.DismissClinicianInvite))).Methods("DELETE") + + c.Handle("/v1/clinics/{clinicId}/invites/clinicians", vars(a.SendClinicianInvite)).Methods("POST") + c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.ResendClinicianInvite)).Methods("PATCH") + c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.GetClinicianInvite)).Methods("GET") + c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.CancelClinicianInvite)).Methods("DELETE") + + rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians", vars(a.SendClinicianInvite)).Methods("POST") + rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.GetClinicianInvite)).Methods("GET") + rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.ResendClinicianInvite)).Methods("PATCH") + rtr.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.CancelClinicianInvite)).Methods("DELETE") } func (h varsHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { @@ -254,9 +304,7 @@ func (h varsHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { func (a *Api) IsReady(res http.ResponseWriter, req *http.Request) { if err := a.Store.Ping(req.Context()); err != nil { - log.Printf("Error getting status [%v]", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, err.Error())} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, "store connectivity failure", err) return } res.WriteHeader(http.StatusOK) @@ -272,9 +320,7 @@ func (a *Api) IsAlive(res http.ResponseWriter, req *http.Request) { // write an error if it all goes wrong func (a *Api) addOrUpdateConfirmation(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) bool { if err := a.Store.UpsertConfirmation(ctx, conf); err != nil { - log.Printf("Error saving the confirmation [%v]", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return false } return true @@ -285,7 +331,6 @@ func (a *Api) addOrUpdateConfirmation(ctx context.Context, conf *models.Confirma func (a *Api) addProfile(conf *models.Confirmation) error { if conf.CreatorId != "" { if err := a.seagull.GetCollection(conf.CreatorId, "profile", a.sl.TokenProvide(), &conf.Creator.Profile); err != nil { - log.Printf("error getting the creators profile [%v] ", err) return err } @@ -294,28 +339,14 @@ func (a *Api) addProfile(conf *models.Confirmation) error { return nil } -// Find these confirmations -// write error if fails or write no-content if it doesn't exist -func (a *Api) checkFoundConfirmations(res http.ResponseWriter, results []*models.Confirmation, err error) []*models.Confirmation { - if err != nil { - log.Println("Error finding confirmations ", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) - return nil - } else if len(results) == 0 { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_NOT_FOUND)} - //log.Println("No confirmations were found ", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - return nil - } else { - for i := range results { - if err = a.addProfile(results[i]); err != nil { - //report and move on - log.Println("Error getting profile", err.Error()) - } +func (a *Api) addProfileInfoToConfirmations(results []*models.Confirmation) []*models.Confirmation { + for i := range results { + if err := a.addProfile(results[i]); err != nil { + //report and move on + a.logger.With(zap.Error(err)).Warn("getting profile") } - return results } + return results } // Generate a notification from the given confirmation,write the error if it fails @@ -329,7 +360,7 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma templateName = models.TemplateNameCareteamInvite has, err := conf.HasPermission("follow") if err != nil { - log.Printf("error checking permissions, will fallback to non-alerting: %s", err) + a.logger.With(zap.Error(err)).Warn("permissions check failed; falling back to non-alerting notification") } else if has { templateName = models.TemplateNameCareteamInviteWithAlerting } @@ -338,7 +369,8 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma case models.TypeNoAccount: templateName = models.TemplateNameNoAccount default: - log.Printf("Unknown confirmation type %s", conf.Type) + a.logger.With(zap.String("type", string(conf.Type))). + Info("unknown confirmation type") return false } } @@ -348,13 +380,14 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma template, ok := a.templates[templateName] if !ok { - log.Printf("Unknown template type %s", templateName) + a.logger.With(zap.String("template", string(templateName))). + Info("unknown template type") return false } subject, body, err := template.Execute(content) if err != nil { - log.Printf("Error executing email template %s", err) + a.logger.With(zap.Error(err)).Error("executing email template") return false } @@ -385,16 +418,14 @@ func (a *Api) token(res http.ResponseWriter, req *http.Request) *shoreline.Token td := a.sl.CheckToken(token) if td == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusForbidden, STATUS_INVALID_TOKEN)} - log.Printf("token %v err[%v] ", token, statusErr) - a.sendModelAsResWithStatus(res, statusErr, http.StatusForbidden) + a.sendError(res, http.StatusForbidden, STATUS_INVALID_TOKEN, + zap.String("token", token)) return nil } //all good! return td } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_NO_TOKEN)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_NO_TOKEN) return nil } @@ -416,7 +447,7 @@ func (a *Api) logMetricAsServer(name string) { // The indentifier could be either an id or email address func (a *Api) findExistingUser(indentifier, token string) *shoreline.UserData { if usr, err := a.sl.GetUser(indentifier, token); err != nil { - log.Printf("Error [%s] trying to get existing users details", err.Error()) + a.logger.With(zap.Error(err)).Error("getting user details") return nil } else { return usr @@ -432,9 +463,11 @@ func (a *Api) ensureIdSet(ctx context.Context, userId string, confirmations []*m for i := range confirmations { //set the userid if not set already if confirmations[i].UserId == "" { - log.Println("UserId wasn't set for invite so setting it") + a.logger.Debug("UserId wasn't set for invite so setting it") confirmations[i].UserId = userId - a.Store.UpsertConfirmation(ctx, confirmations[i]) + if err := a.Store.UpsertConfirmation(ctx, confirmations[i]); err != nil { + a.logger.With(zap.Error(err)).Warn("upserting confirmation") + } } } } @@ -489,7 +522,7 @@ func (a *Api) populateRestrictions(ctx context.Context, user shoreline.UserData, func (a *Api) sendModelAsResWithStatus(res http.ResponseWriter, model interface{}, statusCode int) { if jsonDetails, err := json.Marshal(model); err != nil { - log.Printf("Error [%s] trying to send model [%s]", err.Error(), model) + a.logger.With("model", model, zap.Error(err)).Errorf("trying to send model") http.Error(res, "Error marshaling data for response", http.StatusInternalServerError) } else { res.Header().Set("content-type", "application/json") @@ -499,43 +532,60 @@ func (a *Api) sendModelAsResWithStatus(res http.ResponseWriter, model interface{ } func (a *Api) sendError(res http.ResponseWriter, statusCode int, reason string, extras ...interface{}) { - errs := []error{} - nonErrs := []interface{}{} - for _, extra := range extras { - if err, ok := extra.(error); ok { - errs = append(errs, err) - } else { - nonErrs = append(nonErrs, extra) - } - } - a.logger. - Desugar(). - WithOptions(zap.AddCallerSkip(1)). - Sugar(). - With(zap.Int("code", statusCode)). - With(zap.Errors("errors", errs)). - With("extras", nonErrs). - Error(reason) + a.sendErrorLog(statusCode, reason, extras...) a.sendModelAsResWithStatus(res, status.NewStatus(statusCode, reason), statusCode) } func (a *Api) sendErrorWithCode(res http.ResponseWriter, statusCode int, errorCode int, reason string, extras ...interface{}) { - _, file, line, ok := runtime.Caller(1) - if ok { - segments := strings.Split(file, "/") - file = segments[len(segments)-1] + a.sendErrorLog(statusCode, reason, extras...) + a.sendModelAsResWithStatus(res, status.NewStatusWithError(statusCode, errorCode, reason), statusCode) +} + +func (a *Api) sendErrorLog(code int, reason string, extras ...interface{}) { + nonErrs, errs, fields := splitExtrasAndErrorsAndFields(extras) + log := a.logger.WithOptions(zap.AddCallerSkip(2)). + Desugar().With(fields...).Sugar(). + With(zap.Int("code", code)). + With(zap.Array("extras", zapArrayAny(nonErrs))) + if len(errs) == 1 { + log = log.With(zap.Error(errs[0])) + } else if len(errs) > 1 { + log = log.With(zap.Errors("errors", errs)) + } + if code < http.StatusInternalServerError || len(errs) == 0 { + // if there are no errors, use info to skip the stack trace, as it's + // probably not useful + log.Info(reason) } else { - file = "???" - line = 0 + log.Error(reason) } +} - messages := make([]string, len(extras)) - for index, extra := range extras { - messages[index] = fmt.Sprintf("%v", extra) - } +// sendOK helps send a 200 response with a standard form and optional message. +func (a *Api) sendOK(res http.ResponseWriter, reason string) { + a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusOK, reason), http.StatusOK) +} - log.Printf("%s:%d RESPONSE ERROR: [%d %s] %s", file, line, statusCode, reason, strings.Join(messages, "; ")) - a.sendModelAsResWithStatus(res, status.NewStatusWithError(statusCode, errorCode, reason), statusCode) +func splitExtrasAndErrorsAndFields(extras []interface{}) ([]interface{}, []error, []zapcore.Field) { + errs := []error{} + nonErrs := []interface{}{} + fields := []zap.Field{} + for _, extra := range extras { + if err, ok := extra.(error); ok { + if err != nil { + errs = append(errs, err) + } + } else if field, ok := extra.(zap.Field); ok { + fields = append(fields, field) + } else if extraErrs, ok := extra.([]error); ok { + if len(extraErrs) > 0 { + errs = append(errs, extraErrs...) + } + } else { + nonErrs = append(nonErrs, extra) + } + } + return nonErrs, errs, fields } func (a *Api) tokenUserHasRequestedPermissions(tokenData *shoreline.TokenData, groupId string, requestedPermissions commonClients.Permissions) (commonClients.Permissions, error) { @@ -555,3 +605,14 @@ func (a *Api) tokenUserHasRequestedPermissions(tokenData *shoreline.TokenData, g return finalPermissions, nil } } + +// zapArrayAny helps convert extras to strings for inclusion in a structured +// log message. +func zapArrayAny(extras []interface{}) zapcore.ArrayMarshalerFunc { + return zapcore.ArrayMarshalerFunc(func(enc zapcore.ArrayEncoder) error { + for _, extra := range extras { + enc.AppendString(fmt.Sprintf("%v", extra)) + } + return nil + }) +} diff --git a/api/hydrophoneApi_test.go b/api/hydrophoneApi_test.go index 910bddfaf..8bce565e0 100644 --- a/api/hydrophoneApi_test.go +++ b/api/hydrophoneApi_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "reflect" @@ -14,7 +13,6 @@ import ( "github.com/golang/mock/gomock" "github.com/gorilla/mux" "go.uber.org/fx" - "go.uber.org/zap" clinicsClient "github.com/tidepool-org/clinic/client" commonClients "github.com/tidepool-org/go-common/clients" @@ -23,6 +21,7 @@ import ( "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/clients" "github.com/tidepool-org/hydrophone/models" + "github.com/tidepool-org/hydrophone/testutil" "github.com/tidepool-org/platform/alerts" ) @@ -80,6 +79,7 @@ var ( MockAlertsModule, MockTemplatesModule, MockConfigModule, + fx.Provide(testutil.NewLogger), fx.Provide(NewApi), fx.Provide(mux.NewRouter), ) @@ -100,7 +100,6 @@ func TestGetStatus_StatusOk(t *testing.T) { BaseModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&api), ) @@ -122,7 +121,6 @@ func TestGetStatus_StatusInternalServerError(t *testing.T) { BaseModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&api), ) @@ -135,10 +133,13 @@ func TestGetStatus_StatusInternalServerError(t *testing.T) { t.Fatalf("Resp given [%d] expected [%d] ", response.Code, http.StatusInternalServerError) } - body, _ := ioutil.ReadAll(response.Body) + body, err := io.ReadAll(response.Body) + if err != nil { + t.Fatalf("reading response body: %s", err) + } - if string(body) != `{"code":500,"reason":"Session failure"}` { - t.Fatalf("Message given [%s] expected [%s] ", string(body), "Session failure") + if string(body) != `{"code":500,"reason":"store connectivity failure"}` { + t.Fatalf("Message given [%s] expected [%s] ", string(body), "store connectivity failure") } } @@ -220,7 +221,6 @@ func Test_TokenUserHasRequestedPermissions_GatekeeperError(t *testing.T) { ResponableModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&responsableHydrophone), fx.Populate(&gk), ) @@ -250,7 +250,6 @@ func Test_TokenUserHasRequestedPermissions_CompleteMismatch(t *testing.T) { ResponableModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&responsableHydrophone), fx.Populate(&gk), ) @@ -277,7 +276,6 @@ func Test_TokenUserHasRequestedPermissions_PartialMismatch(t *testing.T) { ResponableModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&responsableHydrophone), fx.Populate(&gk), ) @@ -304,7 +302,6 @@ func Test_TokenUserHasRequestedPermissions_FullMatch(t *testing.T) { ResponableModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&responsableHydrophone), fx.Populate(&gk), ) diff --git a/api/invite.go b/api/invite.go index d153f0693..c2e1efc52 100644 --- a/api/invite.go +++ b/api/invite.go @@ -4,15 +4,14 @@ import ( "context" "encoding/json" "fmt" - "log" "net/http" "go.uber.org/zap" clinics "github.com/tidepool-org/clinic/client" + "github.com/tidepool-org/go-common/clients" commonClients "github.com/tidepool-org/go-common/clients" "github.com/tidepool-org/go-common/clients/shoreline" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) @@ -50,8 +49,7 @@ func (a *Api) checkForDuplicateInvite(ctx context.Context, inviteeEmail, invitor //rule is we cannot send if the invite is not yet expired if !invites[0].IsExpired() { - log.Println(statusExistingInviteMessage) - log.Println("last invite not yet expired") + a.logger.With(zap.String("email", inviteeEmail)).Debug(statusExistingInviteMessage) return true } } @@ -78,9 +76,8 @@ func (a *Api) checkAccountAlreadySharedWithUser(invitorID, inviteeEmail string) if invitedUsr != nil && invitedUsr.UserID != "" { if perms, err := a.gatekeeper.UserInGroup(invitedUsr.UserID, invitorID); err != nil { - log.Printf("error checking if user is in group [%v]", err) + a.logger.With(zap.Error(err)).Error("checking if user is in group") } else if perms != nil { - log.Println(statusExistingMemberMessage) return true, invitedUsr } return false, invitedUsr @@ -104,8 +101,8 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, } // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - a.logger.Warnf("token owner %s is not authorized to accept invite of for %s", token.UserID, inviteeID) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + zap.String("inviteeID", inviteeID)) return } @@ -114,14 +111,18 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, //find all oustanding invites were this user is the invite// found, err := a.Store.FindConfirmations(req.Context(), &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeCareteamInvite}, models.StatusPending) if err != nil { - a.logger.Errorw("error while finding pending invites", zap.Error(err)) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + return } - - if invites := a.checkFoundConfirmations(res, found, err); invites != nil { + if len(found) == 0 { + a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + if invites := a.addProfileInfoToConfirmations(found); invites != nil { a.ensureIdSet(req.Context(), inviteeID, invites) - a.logger.Infof("found and have checked [%d] invites ", len(invites)) a.logMetric("get received invites", req) a.sendModelAsResWithStatus(res, invites, http.StatusOK) + a.logger.Debugf("invites found and checked: %d", len(invites)) return } } @@ -144,7 +145,7 @@ func (a *Api) GetSentInvitations(res http.ResponseWriter, req *http.Request, var } if permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -153,7 +154,15 @@ func (a *Api) GetSentInvitations(res http.ResponseWriter, req *http.Request, var //find all invites I have sent that are pending or declined found, err := a.Store.FindConfirmations(req.Context(), &models.Confirmation{CreatorId: invitorID, Type: models.TypeCareteamInvite}, models.StatusPending, models.StatusDeclined) - if invitations := a.checkFoundConfirmations(res, found, err); invitations != nil { + if err != nil { + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + return + } + if len(found) == 0 { + a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + if invitations := a.addProfileInfoToConfirmations(found); invitations != nil { a.logMetric("get sent invites", req) a.sendModelAsResWithStatus(res, invitations, http.StatusOK) return @@ -173,36 +182,29 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ invitorID := vars["invitedby"] if inviteeID == "" || invitorID == "" { - log.Printf("AcceptInvite inviteeID %s or invitorID %s not set", inviteeID, invitorID) res.WriteHeader(http.StatusBadRequest) + a.logger. + With(zap.String("inviteeID", inviteeID)). + With(zap.String("invitorID", invitorID)). + Info("inviteeID or invitorID is not set") return } // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - log.Println("AcceptInvite ", STATUS_UNAUTHORIZED) - a.sendModelAsResWithStatus( - res, - status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, - http.StatusUnauthorized, - ) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } accept := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(accept); err != nil { - log.Printf("AcceptInvite error decoding invite data: %v\n", err) - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)}, - http.StatusBadRequest, - ) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } if accept.Key == "" { - log.Println("AcceptInvite has no confirmation key set") res.WriteHeader(http.StatusBadRequest) + a.logger.Info("no confirmation key set") return } @@ -212,9 +214,7 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ return } if conf == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - log.Println("AcceptInvite ", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -226,61 +226,37 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ ValidateCreatorID(invitorID, &validationErrors) if len(validationErrors) > 0 { - for _, validationError := range validationErrors { - log.Println("AcceptInvite forbidden as there was a expectation mismatch", validationError) - } - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)}, - http.StatusForbidden, - ) + a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + zap.Errors("validation-errors", validationErrors)) return } ctc := &models.CareTeamContext{} if err := conf.DecodeContext(ctc); err != nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONTEXT)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONTEXT) return } if err := ctc.Validate(); err != nil { - log.Printf("AcceptInvite error validating CareTeamContext: %s", err) - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_VALIDATING_CONTEXT)}, - http.StatusForbidden, - ) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_VALIDATING_CONTEXT, err) return } setPerms, err := a.gatekeeper.SetPermissions(inviteeID, invitorID, ctc.Permissions) if err != nil { - log.Printf("AcceptInvite error setting permissions [%v]", err) - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_DECODING_CONFIRMATION)}, - http.StatusInternalServerError, - ) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SETTING_PERMISSIONS, err) return } - log.Printf("AcceptInvite: permissions were set as [%v] after an invite was accepted", setPerms) + a.logger.With(zapPermsField(setPerms)).Info("permissions set") if ctc.AlertsConfig != nil && ctc.Permissions["follow"] != nil { if err := a.alerts.Upsert(req.Context(), ctc.AlertsConfig); err != nil { - log.Printf("AcceptInvite: error creating alerting config: %s", err) - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_CREATING_ALERTS_CONFIG)}, - http.StatusInternalServerError, - ) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_ALERTS_CONFIG, err) return } } conf.UpdateStatus(models.StatusCompleted) if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - log.Println("AcceptInvite ", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } a.logMetric("acceptinvite", req) @@ -307,7 +283,7 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ } if permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -336,9 +312,7 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ return } } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - log.Printf("CancelInvite: [%s]", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -358,16 +332,13 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - log.Printf("DismissInvite %s ", STATUS_UNAUTHORIZED) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } dismiss := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(dismiss); err != nil { - log.Printf("DismissInvite: error decoding invite to dismiss [%v]", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -390,9 +361,7 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map return } } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - log.Printf("DismissInvite: [%s]", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -421,7 +390,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st } permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, requiredPerms) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -430,9 +399,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st var ib = &inviteBody{} if err := json.NewDecoder(req.Body).Decode(ib); err != nil { - log.Printf("SendInvite: error decoding invite to detail %v\n", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -442,11 +409,8 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st } if a.checkForDuplicateInvite(req.Context(), ib.Email, invitorID) { - log.Printf("SendInvite: invited [%s] user already has or had an invite", ib.Email) - statusErr := &status.StatusError{ - Status: status.NewStatus(http.StatusConflict, statusExistingInviteMessage), - } - a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict) + a.sendError(res, http.StatusConflict, statusExistingInviteMessage, + zap.String("email", ib.Email)) return } alreadyMember, invitedUsr := a.checkAccountAlreadySharedWithUser(invitorID, ib.Email) @@ -467,15 +431,15 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st // Since this invitation doesn't add alerting permissions, // maintain the previous handler's behavior, and abort with an // error response. - a.logger.Infof("invited [%s] user is already a member of the care team of %v", ib.Email, invitorID) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusConflict, statusExistingMemberMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict) + a.sendError(res, http.StatusConflict, statusExistingMemberMessage, + zap.String("email", ib.Email), zap.String("invitorID", invitorID)) return } + for key := range perms { - log.Printf("adding permission: %q %+v", key, perms[key]) ib.Permissions[key] = perms[key] } + a.logger.With(zapPermsField(perms)).Info("permissions set") } templateName := models.TemplateNameCareteamInvite @@ -485,8 +449,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st invite, err := models.NewConfirmationWithContext(models.TypeCareteamInvite, templateName, invitorID, ib.CareTeamContext) if err != nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, statusInternalServerErrorMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } @@ -501,7 +464,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st a.logMetric("invite created", req) if err := a.addProfile(invite); err != nil { - log.Println("SendInvite: ", err.Error()) + a.logger.With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) a.sendModelAsResWithStatus(res, invite, http.StatusOK) } @@ -561,18 +524,16 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ } if invite == nil || invite.ClinicId != "" { if invite.ClinicId != "" { - a.logger.Warn("cannot resend clinic invite using care team invite endpoint") + a.logger.Info("cannot resend clinic invite using care team invite endpoint") } else { - a.logger.Warn("cannot resend confirmation, because it doesn't exist") + a.logger.Info("cannot resend confirmation, because it doesn't exist") } - - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusForbidden) + a.sendError(res, http.StatusForbidden, statusForbiddenMessage) return } if permissions, err := a.tokenUserHasRequestedPermissions(token, invite.CreatorId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusForbidden, statusForbiddenMessage) @@ -584,7 +545,7 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ a.logMetric("invite updated", req) if err := a.addProfile(invite); err != nil { - a.logger.Warn("Resend invite", zap.Error(err)) + a.logger.With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) } else { fullName := invite.Creator.Profile.FullName if invite.Creator.Profile.Patient.IsOtherPerson { @@ -612,3 +573,11 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ } } } + +func zapPermsField(perms clients.Permissions) zap.Field { + permsForLog := []string{} + for key := range perms { + permsForLog = append(permsForLog, key) + } + return zap.Strings("perms", permsForLog) +} diff --git a/api/invite_test.go b/api/invite_test.go index bd0273f8e..dfe13f5c3 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -11,14 +11,14 @@ import ( "time" "github.com/gorilla/mux" - "go.uber.org/zap" commonClients "github.com/tidepool-org/go-common/clients" "github.com/tidepool-org/hydrophone/clients" "github.com/tidepool-org/hydrophone/models" + "github.com/tidepool-org/hydrophone/testutil" ) -func initTestingRouterNoPerms() *mux.Router { +func initTestingRouterNoPerms(t *testing.T) *mux.Router { testRtr := mux.NewRouter() hydrophone := NewApi( FAKE_CONFIG, @@ -31,7 +31,7 @@ func initTestingRouterNoPerms() *mux.Router { mockSeagull, nil, mockTemplates, - zap.NewNop().Sugar(), + testutil.NewLogger(t), ) hydrophone.SetHandlers("", testRtr) return testRtr @@ -39,7 +39,7 @@ func initTestingRouterNoPerms() *mux.Router { func TestSendInvite_NoPerms(t *testing.T) { - tstRtr := initTestingRouterNoPerms() + tstRtr := initTestingRouterNoPerms(t) sendBody := &bytes.Buffer{} json.NewEncoder(sendBody).Encode(testJSONObject{ "email": testing_uid2 + "@email.org", @@ -62,7 +62,7 @@ func TestSendInvite_NoPerms(t *testing.T) { func TestGetReceivedInvitations_NoPerms(t *testing.T) { - tstRtr := initTestingRouterNoPerms() + tstRtr := initTestingRouterNoPerms(t) request := MustRequest(t, "GET", fmt.Sprintf("/invitations/%s", testing_uid2), nil) request.Header.Set(TP_SESSION_TOKEN, testing_uid1) @@ -77,7 +77,7 @@ func TestGetReceivedInvitations_NoPerms(t *testing.T) { func TestGetSentInvitations_NoPerms(t *testing.T) { - tstRtr := initTestingRouterNoPerms() + tstRtr := initTestingRouterNoPerms(t) request := MustRequest(t, "GET", fmt.Sprintf("/invite/%s", testing_uid2), nil) request.Header.Set(TP_SESSION_TOKEN, testing_uid1) @@ -92,7 +92,7 @@ func TestGetSentInvitations_NoPerms(t *testing.T) { func TestAcceptInvite_NoPerms(t *testing.T) { - tstRtr := initTestingRouterNoPerms() + tstRtr := initTestingRouterNoPerms(t) request := MustRequest(t, "PUT", fmt.Sprintf("/accept/invite/%s/%s", testing_uid2, testing_uid1), nil) request.Header.Set(TP_SESSION_TOKEN, testing_uid1) @@ -107,7 +107,7 @@ func TestAcceptInvite_NoPerms(t *testing.T) { func TestDismissInvite_NoPerms(t *testing.T) { - tstRtr := initTestingRouterNoPerms() + tstRtr := initTestingRouterNoPerms(t) request := MustRequest(t, "PUT", fmt.Sprintf("/dismiss/invite/%s/%s", testing_uid2, testing_uid1), nil) request.Header.Set(TP_SESSION_TOKEN, testing_uid1) @@ -241,6 +241,7 @@ func TestInviteResponds(t *testing.T) { }, } + logger := testutil.NewLogger(t) for idx, inviteTest := range inviteTests { // don't run a test if it says to skip it if inviteTest.skip { @@ -248,11 +249,16 @@ func TestInviteResponds(t *testing.T) { } var testRtr = mux.NewRouter() + store := mockStore //default flow, fully authorized + if inviteTest.returnNone { + //testing when there is nothing to return from the store + store = mockStoreEmpty + } hydrophone := NewApi( FAKE_CONFIG, nil, - mockStore, + store, mockNotifier, mockShoreline, mockGatekeeper, @@ -260,26 +266,9 @@ func TestInviteResponds(t *testing.T) { mockSeagull, nil, mockTemplates, - zap.NewNop().Sugar(), + logger, ) - //testing when there is nothing to return from the store - if inviteTest.returnNone { - hydrophone = NewApi( - FAKE_CONFIG, - nil, - mockStoreEmpty, - mockNotifier, - mockShoreline, - mockGatekeeper, - mockMetrics, - mockSeagull, - nil, - mockTemplates, - zap.NewNop().Sugar(), - ) - } - hydrophone.SetHandlers("", testRtr) var body = &bytes.Buffer{} @@ -338,7 +327,7 @@ func TestInviteCanAddAlerting(t *testing.T) { mockSeagull, nil, mockTemplates, - zap.NewNop().Sugar(), + testutil.NewLogger(t), ) testRtr := mux.NewRouter() hydrophone.SetHandlers("", testRtr) @@ -411,7 +400,7 @@ func TestAcceptInviteAlertsConfigRequiresFollowPerm(t *testing.T) { mockSeagull, newMockAlertsClientWithFailingUpsert(), mockTemplates, - zap.NewNop().Sugar(), + testutil.NewLogger(t), ) c := &models.Confirmation{ Key: testing_uid2, @@ -443,8 +432,8 @@ func TestAcceptInviteAlertsConfigRequiresFollowPerm(t *testing.T) { testRtr.ServeHTTP(response, request) - if response.Code != http.StatusForbidden { - t.Fatalf("expected status `%d` actual `%d`", http.StatusForbidden, response.Code) + if response.Code != http.StatusBadRequest { + t.Fatalf("expected status `%d` actual `%d`", http.StatusBadRequest, response.Code) } if numCalls := len(mockStoreAlerting.Calls["UpsertConfirmation"]); numCalls != 0 { @@ -471,7 +460,7 @@ func TestAcceptInviteAlertsConfigOptional(t *testing.T) { mockSeagull, newMockAlertsClientWithFailingUpsert(), mockTemplates, - zap.NewNop().Sugar(), + testutil.NewLogger(t), ) c := &models.Confirmation{ Key: testing_uid2, @@ -533,7 +522,7 @@ func TestInviteAddingAlertingMergesPerms(t *testing.T) { mockSeagull, nil, mockTemplates, - zap.NewNop().Sugar(), + testutil.NewLogger(t), ) testRtr := mux.NewRouter() hydrophone.SetHandlers("", testRtr) diff --git a/api/patientInvites.go b/api/patientInvites.go index 6c5f02b21..9a5351a5e 100644 --- a/api/patientInvites.go +++ b/api/patientInvites.go @@ -9,7 +9,6 @@ import ( clinics "github.com/tidepool-org/clinic/client" commonClients "github.com/tidepool-org/go-common/clients" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) @@ -25,20 +24,24 @@ func (a *Api) GetPatientInvites(res http.ResponseWriter, req *http.Request, vars } if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { - a.logger.Errorw("token owner is not a clinic member", zap.Error(err)) return } // find all outstanding invites that are associated to this clinic found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{ClinicId: clinicId, Type: models.TypeCareteamInvite}, models.StatusPending) - if err == nil && len(found) == 0 { + if err != nil { + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + return + } + if len(found) == 0 { result := make([]*models.Confirmation, 0) a.sendModelAsResWithStatus(res, result, http.StatusOK) return - } else if invites := a.checkFoundConfirmations(res, found, err); invites != nil { - a.logger.Infof("found and checked %d confirmations", len(invites)) + } + if invites := a.addProfileInfoToConfirmations(found); invites != nil { a.logMetric("get_patient_invites", req) a.sendModelAsResWithStatus(res, invites, http.StatusOK) + a.logger.Debugf("confirmations found and checked: %d", len(invites)) return } } @@ -57,7 +60,6 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va } if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { - a.logger.Errorw("token owner is not a clinic member", zap.Error(err)) return } @@ -72,9 +74,7 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va return } if conf == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) - a.logger.With(zap.Error(statusErr)).Info(statusInviteNotFoundMessage) + a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -84,33 +84,20 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va conf.ValidateClinicID(clinicId, &validationErrors) if len(validationErrors) > 0 { - for _, validationError := range validationErrors { - a.logger.Warnw("forbidden as there was a expectation mismatch", zap.Error(validationError)) - } - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)}, - http.StatusForbidden, - ) + a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + zap.Errors("validation-errors", validationErrors)) return } patient, err := a.createClinicPatient(ctx, *conf) if err != nil { - a.logger.Errorw("error creating patient", zap.Error(err)) - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_CREATING_PATIENT)}, - http.StatusInternalServerError, - ) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_PATIENT, err) return } conf.UpdateStatus(models.StatusCompleted) if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.logger.Warn("error adding or updating confirmation") - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } @@ -143,9 +130,7 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re return } if conf == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusForbidden) - a.logger.With(zap.Error(statusErr)).Info(statusInviteNotFoundMessage) + a.sendError(res, http.StatusForbidden, statusInviteNotFoundMessage) return } @@ -153,7 +138,6 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re if token.UserID != conf.CreatorId { updatedStatus = models.StatusDeclined if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { - a.logger.Errorw("token owner is not a clinic member", zap.Error(err)) return } } @@ -164,22 +148,13 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re conf.ValidateClinicID(clinicId, &validationErrors) if len(validationErrors) > 0 { - for _, validationError := range validationErrors { - a.logger.Warnw("forbidden as there was a expectation mismatch", zap.Error(validationError)) - } - a.sendModelAsResWithStatus( - res, - &status.StatusError{Status: status.NewStatus(http.StatusForbidden, statusForbiddenMessage)}, - http.StatusForbidden, - ) + a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + zap.Errors("validation-errors", validationErrors)) return } conf.UpdateStatus(updatedStatus) if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.logger.Warn("error adding or updating confirmation") - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) return } @@ -229,7 +204,7 @@ func (a *Api) createClinicPatient(ctx context.Context, confirmation models.Confi patient = response.JSON200 } - a.logger.Infof("permissions were set as [%v] after an invite was accepted", patient.Permissions) + a.logger.With(zap.Any("perms", patient.Permissions)).Info("permissions set") return patient, nil } diff --git a/api/signup.go b/api/signup.go index 0e0434353..a998b2d27 100644 --- a/api/signup.go +++ b/api/signup.go @@ -4,47 +4,20 @@ import ( "context" "encoding/json" "io" - "log" "net/http" "regexp" "time" + "go.uber.org/zap" + clinics "github.com/tidepool-org/clinic/client" commonClients "github.com/tidepool-org/go-common/clients" "github.com/tidepool-org/go-common/clients/shoreline" - "github.com/tidepool-org/go-common/clients/status" "github.com/tidepool-org/hydrophone/models" ) -const ( - STATUS_SIGNUP_NOT_FOUND = "No matching signup confirmation was found" - STATUS_SIGNUP_NO_ID = "Required userid is missing" - STATUS_SIGNUP_NO_CONF = "Required confirmation id is missing" - STATUS_SIGNUP_ACCEPTED = "User has had signup confirmed" - STATUS_EXISTING_SIGNUP = "User already has an existing valid signup confirmation" - STATUS_SIGNUP_EXPIRED = "The signup confirmation has expired" - STATUS_SIGNUP_ERROR = "Error while completing signup confirmation. The signup confirmation remains active until it expires" - STATUS_ERR_FINDING_USR = "Error finding user" - STATUS_ERR_UPDATING_USR = "Error updating user" - STATUS_NO_PASSWORD = "User does not have a password" - STATUS_MISSING_PASSWORD = "Password is missing" - STATUS_INVALID_PASSWORD = "Password specified is invalid" - STATUS_MISSING_BIRTHDAY = "Birthday is missing" - STATUS_INVALID_BIRTHDAY = "Birthday specified is invalid" - STATUS_MISMATCH_BIRTHDAY = "Birthday specified does not match patient birthday" -) - -const ( - ERROR_NO_PASSWORD = 1001 - ERROR_MISSING_PASSWORD = 1002 - ERROR_INVALID_PASSWORD = 1003 - ERROR_MISSING_BIRTHDAY = 1004 - ERROR_INVALID_BIRTHDAY = 1005 - ERROR_MISMATCH_BIRTHDAY = 1006 -) - // try to find the signup confirmation func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) *models.Confirmation { found, err := a.Store.FindConfirmation(ctx, conf) @@ -53,9 +26,7 @@ func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res htt return nil } if found == nil { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND)} - log.Printf("findSignUp: not found [%s]\n", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return nil } @@ -66,16 +37,12 @@ func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res htt func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.ResponseWriter, req *http.Request) { fromBody := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(fromBody); err != nil { - log.Printf("updateSignupConfirmation: error decoding signup to cancel [%s]", err.Error()) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } if fromBody.Key == "" { - log.Printf("updateSignupConfirmation: %s", STATUS_SIGNUP_NO_CONF) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_CONF)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) return } @@ -86,7 +53,6 @@ func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.Respons } if found != nil { updatedStatus := string(newStatus) + " signup" - log.Printf("updateSignupConfirmation: %s", updatedStatus) found.UpdateStatus(newStatus) if a.addOrUpdateConfirmation(req.Context(), found, res) { @@ -95,8 +61,7 @@ func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.Respons return } } else { - log.Printf("updateSignupConfirmation: %s [%v]", STATUS_SIGNUP_NOT_FOUND, fromBody) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND), http.StatusNotFound) + a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return } } @@ -129,8 +94,7 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st } if err := a.addProfile(newSignUp); err != nil { - log.Printf("sendSignUp: error when adding profile [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) return } if newSignUp.Creator.Profile != nil && newSignUp.Creator.Profile.FullName != "" { @@ -139,11 +103,14 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st profile := &models.Profile{} if err := a.seagull.GetCollection(newSignUp.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, "sendSignUp: error getting user profile: ", err.Error()) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting user profile", err) return } - log.Printf("Sending email confirmation to %s with key %s", newSignUp.Email, newSignUp.Key) + a.logger. + With(zap.String("email", newSignUp.Email)). + With(zap.String("key", newSignUp.Key)). + Debug("sending email confirmation") emailContent := map[string]interface{}{ "Key": newSignUp.Key, @@ -161,7 +128,6 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st return } else { a.logMetric("signup confirmation failed to be sent", req) - log.Print("Something happened generating a signup email") } } } @@ -178,14 +144,12 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ if found := a.findSignUp(req.Context(), toFind, res); found != nil { if err := a.Store.RemoveConfirmation(req.Context(), found); err != nil { - log.Printf("resendSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return } if err := found.ResetKey(); err != nil { - log.Printf("resendSignUp: error resetting key [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return } @@ -193,17 +157,19 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ a.logMetricAsServer("signup confirmation recreated") if err := a.addProfile(found); err != nil { - log.Printf("resendSignUp: error when adding profile [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) return } else { profile := &models.Profile{} if err := a.seagull.GetCollection(found.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, "resendSignUp: error getting user profile: ", err.Error()) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } - log.Printf("Resending email confirmation to %s with key %s", found.Email, found.Key) + a.logger. + With(zap.String("email", found.Email)). + With(zap.String("key", found.Key)). + Debug("resending email confirmation") emailContent := map[string]interface{}{ "Key": found.Key, @@ -230,7 +196,6 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ a.logMetricAsServer("signup confirmation re-sent") } else { a.logMetricAsServer("signup confirmation failed to be sent") - log.Print("resendSignUp: Something happened trying to resend a signup email") } } @@ -255,8 +220,7 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ confirmationId := vars["confirmationid"] if confirmationId == "" { - log.Printf("acceptSignUp %s", STATUS_SIGNUP_NO_CONF) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_CONF), http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) return } @@ -264,7 +228,7 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ if found := a.findSignUp(req.Context(), toFind, res); found != nil { if found.IsExpired() { - a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_EXPIRED, "acceptSignUp: expired") + a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_EXPIRED) return } @@ -272,43 +236,43 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ updates := shoreline.UserUpdate{EmailVerified: &emailVerified} if user, err := a.sl.GetUser(found.UserId, a.sl.TokenProvide()); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, "acceptSignUp: error trying to get user to check email verified: ", err.Error()) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "trying to get user to check email verified", err) return } else if !user.PasswordExists { acceptance := &models.Acceptance{} if req.Body != nil { if err := json.NewDecoder(req.Body).Decode(acceptance); err != nil { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_NO_PASSWORD, STATUS_NO_PASSWORD, "acceptSignUp: error decoding acceptance: ", err.Error()) + a.sendErrorWithCode(res, http.StatusConflict, ERROR_NO_PASSWORD, STATUS_NO_PASSWORD, "decoding acceptance", err) return } } if acceptance.Password == "" { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_PASSWORD, STATUS_MISSING_PASSWORD, "acceptSignUp: missing password") + a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_PASSWORD, STATUS_MISSING_PASSWORD, "missing password") return } if !IsValidPassword(acceptance.Password) { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_PASSWORD, STATUS_INVALID_PASSWORD, "acceptSignUp: invalid password specified") + a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_PASSWORD, STATUS_INVALID_PASSWORD, "invalid password specified") return } if acceptance.Birthday == "" { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_BIRTHDAY, STATUS_MISSING_BIRTHDAY, "acceptSignUp: missing birthday") + a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_BIRTHDAY, STATUS_MISSING_BIRTHDAY, "missing birthday") return } if !IsValidDate(acceptance.Birthday) { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_BIRTHDAY, STATUS_INVALID_BIRTHDAY, "acceptSignUp: invalid birthday specified") + a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_BIRTHDAY, STATUS_INVALID_BIRTHDAY, "invalid birthday specified") return } profile := &models.Profile{} if err := a.seagull.GetCollection(found.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, "acceptSignUp: error getting the users profile: ", err.Error()) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting the users profile", err) return } if acceptance.Birthday != profile.Patient.Birthday { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISMATCH_BIRTHDAY, STATUS_MISMATCH_BIRTHDAY, "acceptSignUp: acceptance birthday does not match user patient birthday") + a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISMATCH_BIRTHDAY, STATUS_MISMATCH_BIRTHDAY, "acceptance birthday does not match user patient birthday") return } @@ -316,7 +280,7 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ } if err := a.sl.UpdateUser(found.UserId, updates, a.sl.TokenProvide()); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_UPDATING_USR, "acceptSignUp error trying to update user to be email verified: ", err.Error()) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_UPDATING_USER, err) return } @@ -342,11 +306,10 @@ func (a *Api) dismissSignUp(res http.ResponseWriter, req *http.Request, vars map userId := vars["userid"] if userId == "" { - log.Printf("dismissSignUp %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } - log.Print("dismissSignUp: dismissing for ", userId) + a.logger.Debug("dismissing invite") a.updateSignupConfirmation(models.StatusDeclined, res, req) } @@ -360,13 +323,12 @@ func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[str userId := vars["userid"] if userId == "" { - log.Printf("getSignUp %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } if permissions, err := a.tokenUserHasRequestedPermissions(token, userId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -380,13 +342,12 @@ func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[str } if signup == nil { - log.Printf("getSignUp %s", STATUS_SIGNUP_NOT_FOUND) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND), http.StatusNotFound) + a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return } else { a.logMetric("get signup", req) - log.Printf("getSignUp found a pending signup for user %s", userId) a.sendModelAsResWithStatus(res, signup, http.StatusOK) + a.logger.Debug("found a pending signup") return } } @@ -407,6 +368,8 @@ func (a *Api) createSignUp(res http.ResponseWriter, req *http.Request, vars map[ // Return a new or refreshed an account signup confirmation // +// If it returns nil, an HTTP response has been sent. +// // status: 400 STATUS_SIGNUP_NO_ID // status: 401 STATUS_NO_TOKEN // status: 403 STATUS_EXISTING_SIGNUP @@ -415,13 +378,12 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ if token := a.token(res, req); token != nil { userId := vars["userid"] if userId == "" { - log.Printf("upsertSignUp %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return nil } if permissions, err := a.tokenUserHasRequestedPermissions(token, userId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return nil } else if permissions["root"] == nil && permissions["custodian"] == nil { a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) @@ -430,26 +392,23 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ var upsertCustodialSignUpInvite UpsertCustodialSignUpInvite if err := json.NewDecoder(req.Body).Decode(&upsertCustodialSignUpInvite); err != nil && err != io.EOF { - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusBadRequest, "error decoding payload")}, http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION) return nil } if usrDetails, err := a.sl.GetUser(userId, a.sl.TokenProvide()); err != nil { - log.Printf("upsertSignUp %s err[%s]", STATUS_ERR_FINDING_USER, err.Error()) - a.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusNotFound, STATUS_ERR_FINDING_USER)}, http.StatusNotFound) + a.sendError(res, http.StatusNotFound, STATUS_ERR_FINDING_USER, err) return nil } else if len(usrDetails.Emails) == 0 { // Delete existing any existing invites if the email address is empty existing, err := a.Store.FindConfirmation(req.Context(), &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) if err != nil { - log.Printf("upsertSignUp: error finding old invite [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } if existing != nil { if err := a.Store.RemoveConfirmation(req.Context(), existing); err != nil { - log.Printf("upsertSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return nil } } @@ -460,8 +419,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ // get any existing confirmations newSignUp, err := a.Store.FindConfirmation(req.Context(), &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) if err != nil { - log.Printf("upsertSignUp: error [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } else if newSignUp == nil { @@ -483,8 +441,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ } else { tokenUserDetails, err := a.sl.GetUser(token.UserID, a.sl.TokenProvide()) if err != nil { - log.Printf("upsertSignUp: error when getting token user [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return nil } @@ -512,14 +469,12 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ } else if newSignUp.Email != usrDetails.Emails[0] { if err := a.Store.RemoveConfirmation(req.Context(), newSignUp); err != nil { - log.Printf("upsertSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return nil } if err := newSignUp.ResetKey(); err != nil { - log.Printf("upsertSignUp: error resetting key [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return nil } @@ -529,8 +484,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ newSignUp.CreatorId = upsertCustodialSignUpInvite.InvitedBy } } else { - log.Printf("upsertSignUp %s", STATUS_EXISTING_SIGNUP) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusForbidden, STATUS_EXISTING_SIGNUP), http.StatusForbidden) + a.sendError(res, http.StatusForbidden, STATUS_EXISTING_SIGNUP) return nil } @@ -550,12 +504,11 @@ func (a *Api) cancelSignUp(res http.ResponseWriter, req *http.Request, vars map[ userId := vars["userid"] if userId == "" { - log.Printf("cancelSignUp: %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } - log.Print("cancelSignUp: canceling for ", userId) a.updateSignupConfirmation(models.StatusCanceled, res, req) + a.logger.Debug("canceled signup") } func IsValidPassword(password string) bool { diff --git a/api/signup_test.go b/api/signup_test.go index a1875daa0..b56e4f173 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -6,9 +6,9 @@ import ( "net/http/httptest" "testing" - "go.uber.org/zap" - "github.com/gorilla/mux" + + "github.com/tidepool-org/hydrophone/testutil" ) func TestSignupResponds(t *testing.T) { @@ -218,6 +218,7 @@ func TestSignupResponds(t *testing.T) { }, } + logger := testutil.NewLogger(t) for idx, test := range tests { if test.skip { @@ -227,13 +228,12 @@ func TestSignupResponds(t *testing.T) { //fresh each time var testRtr = mux.NewRouter() + store := mockStore if test.returnNone { - hydrophoneFindsNothing := NewApi(FAKE_CONFIG, nil, mockStoreEmpty, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, zap.NewNop().Sugar()) - hydrophoneFindsNothing.SetHandlers("", testRtr) - } else { - hydrophone := NewApi(FAKE_CONFIG, nil, mockStore, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, zap.NewNop().Sugar()) - hydrophone.SetHandlers("", testRtr) + store = mockStoreEmpty } + h := NewApi(FAKE_CONFIG, nil, store, mockNotifier, mockShoreline, mockGatekeeper, mockMetrics, mockSeagull, nil, mockTemplates, logger) + h.SetHandlers("", testRtr) var body = &bytes.Buffer{} // build the body only if there is one defined in the test diff --git a/clients/mockNotifier.go b/clients/mockNotifier.go index b25d9d824..f6e422acd 100644 --- a/clients/mockNotifier.go +++ b/clients/mockNotifier.go @@ -2,9 +2,9 @@ package clients import ( "fmt" - "log" "go.uber.org/fx" + "go.uber.org/zap" ) type ( @@ -17,7 +17,7 @@ func NewMockNotifier() Notifier { func (c *MockNotifier) Send(to []string, subject string, msg string) (int, string) { details := fmt.Sprintf("Send subject[%s] with message[%s] to %v", subject, msg, to) - log.Println(details) + zap.S().Info(details) return 200, details } diff --git a/clients/mongoStoreClient.go b/clients/mongoStoreClient.go index 4a2a46faa..e69ad583b 100644 --- a/clients/mongoStoreClient.go +++ b/clients/mongoStoreClient.go @@ -3,7 +3,6 @@ package clients import ( "context" "fmt" - "log" "regexp" "github.com/kelseyhightower/envconfig" @@ -13,6 +12,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/fx" + "go.uber.org/zap" tpMongo "github.com/tidepool-org/go-common/clients/mongo" "github.com/tidepool-org/hydrophone/models" @@ -26,10 +26,11 @@ const ( type MongoStoreClient struct { client *mongo.Client database string + log *zap.SugaredLogger } // NewMongoStoreClient creates a new MongoStoreClient -func NewMongoStoreClient(config *tpMongo.Config) (*MongoStoreClient, error) { +func NewMongoStoreClient(config *tpMongo.Config, log *zap.SugaredLogger) (*MongoStoreClient, error) { connectionString, err := config.ToConnectionString() if err != nil { return nil, errors.Wrap(err, "invalid MongoDB configuration") @@ -44,6 +45,7 @@ func NewMongoStoreClient(config *tpMongo.Config) (*MongoStoreClient, error) { return &MongoStoreClient{ client: mongoClient, database: config.Database, + log: log, }, nil } @@ -61,8 +63,7 @@ func (c *MongoStoreClient) EnsureIndexes(ctx context.Context) error { } if _, err := confirmationsCollection(c).Indexes().CreateMany(ctx, indexes); err != nil { - log.Fatal(err) - return err + c.log.With(zap.Error(err)).Fatal("creating indexes") } return nil @@ -77,8 +78,8 @@ func mongoConfigProvider() (tpMongo.Config, error) { return config, nil } -func mongoStoreProvider(config tpMongo.Config) (StoreClient, error) { - return NewMongoStoreClient(&config) +func mongoStoreProvider(config tpMongo.Config, log *zap.SugaredLogger) (StoreClient, error) { + return NewMongoStoreClient(&config, log) } // MongoModule for dependency injection @@ -144,7 +145,6 @@ func (c *MongoStoreClient) FindConfirmation(ctx context.Context, confirmation *m opts := options.FindOne().SetSort(bson.D{{Key: "created", Value: -1}}) if err = confirmationsCollection(c).FindOne(ctx, query, opts).Decode(&result); err != nil && err != mongo.ErrNoDocuments { - log.Printf("FindConfirmation: something bad happened [%v]", err) return result, err } @@ -189,7 +189,6 @@ func (c *MongoStoreClient) FindConfirmations(ctx context.Context, confirmation * } if err = cursor.All(ctx, &results); err != nil { - log.Printf("FindConfirmations: something bad happened [%v]", err) return results, err } return results, nil diff --git a/clients/mongoStoreClient_test.go b/clients/mongoStoreClient_test.go index 9214e75d0..c66848b86 100644 --- a/clients/mongoStoreClient_test.go +++ b/clients/mongoStoreClient_test.go @@ -7,6 +7,7 @@ import ( "github.com/tidepool-org/go-common/clients/mongo" "github.com/tidepool-org/hydrophone/models" + "github.com/tidepool-org/hydrophone/testutil" ) func TestMongoStoreConfirmationOperations(t *testing.T) { @@ -17,7 +18,7 @@ func TestMongoStoreConfirmationOperations(t *testing.T) { doesNotExist := MustConfirmation(t, models.TypePasswordReset, models.TemplateNamePasswordReset, "123.456") testingConfig := &mongo.Config{ConnectionString: "mongodb://127.0.0.1/confirm_test", Database: "confirm_test"} - mc, err := NewMongoStoreClient(testingConfig) + mc, err := NewMongoStoreClient(testingConfig, testutil.NewLogger(t)) if err != nil { t.Fatalf("we could not create the store: %v", err) } diff --git a/clients/sesNotifier.go b/clients/sesNotifier.go index bfd94ebb3..21cf87cd3 100644 --- a/clients/sesNotifier.go +++ b/clients/sesNotifier.go @@ -1,14 +1,12 @@ package clients import ( - "log" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ses" "github.com/kelseyhightower/envconfig" "go.uber.org/fx" + "go.uber.org/zap" ) const ( @@ -24,6 +22,7 @@ type ( SesNotifier struct { Config *SesNotifierConfig SES *ses.SES + log *zap.SugaredLogger } // SesNotifierConfig contains the static configuration for the Amazon SES service @@ -44,11 +43,11 @@ func notifierConfigProvider() (SesNotifierConfig, error) { return config, nil } -func sesNotifierProvider(config SesNotifierConfig) (Notifier, error) { +func sesNotifierProvider(config SesNotifierConfig, log *zap.SugaredLogger) (Notifier, error) { if config.UseMockNotifier { return NewMockNotifier(), nil } - mail, err := NewSesNotifier(&config) + mail, err := NewSesNotifier(&config, log) return mail, err } @@ -59,7 +58,7 @@ var SesModule = fx.Options( ) // NewSesNotifier creates a new Amazon SES notifier -func NewSesNotifier(cfg *SesNotifierConfig) (*SesNotifier, error) { +func NewSesNotifier(cfg *SesNotifierConfig, log *zap.SugaredLogger) (*SesNotifier, error) { sess, err := session.NewSession(&aws.Config{ Region: aws.String(cfg.Region)}, ) @@ -71,6 +70,7 @@ func NewSesNotifier(cfg *SesNotifierConfig) (*SesNotifier, error) { return &SesNotifier{ Config: cfg, SES: ses.New(sess), + log: log, }, nil } @@ -110,23 +110,7 @@ func (c *SesNotifier) Send(to []string, subject string, msg string) (int, string // Display error messages if they occur. if err != nil { - if aerr, ok := err.(awserr.Error); ok { - switch aerr.Code() { - case ses.ErrCodeMessageRejected: - log.Printf("%v: %v\n", ses.ErrCodeMessageRejected, aerr.Error()) - case ses.ErrCodeMailFromDomainNotVerifiedException: - log.Printf("%v: %v\n", ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error()) - case ses.ErrCodeConfigurationSetDoesNotExistException: - log.Printf("%v: %v\n", ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error()) - default: - log.Println(aerr.Error()) - } - } else { - // Print the error, cast err to awserr.Error to get the Code and - // Message from an error. - log.Println(err.Error()) - } - + c.log.With(zap.Error(err)).Error("sending email") return 400, result.String() } return 200, result.String() diff --git a/events/events.go b/events/events.go index 2a150a8eb..d02325f65 100644 --- a/events/events.go +++ b/events/events.go @@ -2,7 +2,6 @@ package events import ( "context" - "log" "time" "github.com/tidepool-org/go-common/events" @@ -26,7 +25,6 @@ func NewHandler(store clients.StoreClient) events.EventHandler { } func (h *handler) HandleDeleteUserEvent(payload events.DeleteUserEvent) error { - log.Printf("Deleting confirmations for user %v", payload.UserID) ctx, cancel := context.WithTimeout(context.Background(), deleteTimeout) defer cancel() if err := h.store.RemoveConfirmationsForUser(ctx, payload.UserID); err != nil { diff --git a/go.work.sum b/go.work.sum index 89d03c861..ec0d5f9b8 100644 --- a/go.work.sum +++ b/go.work.sum @@ -499,6 +499,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +<<<<<<< HEAD golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -531,6 +532,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +||||||| parent of eb1e981 (move to zap logging) +======= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +>>>>>>> eb1e981 (move to zap logging) golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= diff --git a/hydrophone.go b/hydrophone.go index 7dc1b84d1..7b7346fc4 100644 --- a/hydrophone.go +++ b/hydrophone.go @@ -3,7 +3,6 @@ package main import ( "context" "crypto/tls" - "log" "net/http" "time" @@ -199,6 +198,7 @@ type InvocationParams struct { Config InboundConfig Server *http.Server Consumer ev.EventConsumer + log *zap.SugaredLogger } func startEventConsumer(p InvocationParams) { @@ -206,10 +206,10 @@ func startEventConsumer(p InvocationParams) { OnStart: func(ctx context.Context) error { go func() { if err := p.Consumer.Start(); err != nil { - log.Printf("Unable to start cloud events consumer: %v", err) - log.Printf("Shutting down the service") + p.log.With(zap.Error(err)).Infof("starting cloud events consumer") + p.log.Infof("shutting down the service") if shutdownErr := p.Shutdowner.Shutdown(); shutdownErr != nil { - log.Printf("Failed to shutdown: %v", shutdownErr) + p.log.With(zap.Error(shutdownErr)).Error("failed to shutdown") } } }() @@ -226,7 +226,7 @@ func startShoreline(p InvocationParams) { fx.Hook{ OnStart: func(ctx context.Context) error { if err := p.Shoreline.Start(); err != nil { - log.Printf("Unable to start Shoreline: %v", err) + p.log.With(zap.Error(err)).Error("starting shoreline") return err } return nil @@ -245,10 +245,10 @@ func startServer(p InvocationParams) { OnStart: func(ctx context.Context) error { go func() { if err := p.Server.ListenAndServe(); err != nil { - log.Printf("Server error: %v", err) - log.Printf("Shutting down the service") + p.log.With(zap.Error(err)).Error("while listening") + p.log.Infof("shutting down") if shutdownErr := p.Shutdowner.Shutdown(); shutdownErr != nil { - log.Printf("Failed to shutdown: %v", shutdownErr) + p.log.With(zap.Error(err)).Error("shutting down") } } }() diff --git a/models/confirmation.go b/models/confirmation.go index b0e4289eb..26e881bf7 100644 --- a/models/confirmation.go +++ b/models/confirmation.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log" "time" "github.com/tidepool-org/go-common/clients" @@ -145,7 +144,6 @@ func (c *Confirmation) DecodeContext(data interface{}) error { if c.Context != nil { if err := json.Unmarshal(c.Context, &data); err != nil { - log.Printf("Err: %v\n", err) return err } } @@ -259,7 +257,6 @@ func generateKey() (string, error) { rb := make([]byte, length) if _, err := rand.Read(rb); err != nil { - log.Println(err) return "", err } else { return base64.URLEncoding.EncodeToString(rb), nil diff --git a/testutil/testutil.go b/testutil/testutil.go new file mode 100644 index 000000000..77a935b20 --- /dev/null +++ b/testutil/testutil.go @@ -0,0 +1,17 @@ +package testutil + +import ( + "testing" + + "go.uber.org/zap" +) + +func NewLogger(t *testing.T) *zap.SugaredLogger { + config := zap.NewDevelopmentConfig() + logger, err := config.Build() + if err != nil { + t.Logf("error configuring zap logger: %s", err) + logger = zap.NewNop() + } + return logger.Sugar() +} From b194b9a6efeef55f373b243a4fea68525d6b77d0 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Wed, 1 Nov 2023 14:00:43 -0600 Subject: [PATCH 04/10] InvocationParams requires everything be exported That's the InvocationParams found in from go.uber.org/fx BACK-2500 --- hydrophone.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hydrophone.go b/hydrophone.go index 7b7346fc4..4355469f7 100644 --- a/hydrophone.go +++ b/hydrophone.go @@ -198,7 +198,7 @@ type InvocationParams struct { Config InboundConfig Server *http.Server Consumer ev.EventConsumer - log *zap.SugaredLogger + Log *zap.SugaredLogger } func startEventConsumer(p InvocationParams) { @@ -206,10 +206,10 @@ func startEventConsumer(p InvocationParams) { OnStart: func(ctx context.Context) error { go func() { if err := p.Consumer.Start(); err != nil { - p.log.With(zap.Error(err)).Infof("starting cloud events consumer") - p.log.Infof("shutting down the service") + p.Log.With(zap.Error(err)).Infof("starting cloud events consumer") + p.Log.Infof("shutting down the service") if shutdownErr := p.Shutdowner.Shutdown(); shutdownErr != nil { - p.log.With(zap.Error(shutdownErr)).Error("failed to shutdown") + p.Log.With(zap.Error(shutdownErr)).Error("failed to shutdown") } } }() @@ -226,7 +226,7 @@ func startShoreline(p InvocationParams) { fx.Hook{ OnStart: func(ctx context.Context) error { if err := p.Shoreline.Start(); err != nil { - p.log.With(zap.Error(err)).Error("starting shoreline") + p.Log.With(zap.Error(err)).Error("starting shoreline") return err } return nil @@ -245,10 +245,10 @@ func startServer(p InvocationParams) { OnStart: func(ctx context.Context) error { go func() { if err := p.Server.ListenAndServe(); err != nil { - p.log.With(zap.Error(err)).Error("while listening") - p.log.Infof("shutting down") + p.Log.With(zap.Error(err)).Error("while listening") + p.Log.Infof("shutting down") if shutdownErr := p.Shutdowner.Shutdown(); shutdownErr != nil { - p.log.With(zap.Error(err)).Error("shutting down") + p.Log.With(zap.Error(err)).Error("shutting down") } } }() From ea36951d7e6caf9a040860951516fe7ed6501187 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Mon, 20 Nov 2023 14:17:10 -0700 Subject: [PATCH 05/10] fix Api logging thread-safety issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When migrating to zap logging, the Api.logger was modified by middleware that was adding the request's userID to the logger's context in a way that wasn't thread-safe. The result was that many many non-sensical userIDs were logged in the context, producing log output that wasn't at all useful. The best way to fix this was to properly generate a child logger for each request with the appropriate userID in its logging context, then pass that logger into the handlers via their request's context. As a result, the fix touches a lot of code—anywhere that logs, now needs to retrieve the request-specific logger from that request's context. Tests were written that attempt to exacerbate the problem, but as its a caused by race conditions, it's very hard to reliably reproduce. Flappiness in the test is a red flag though, and comments were added to indicate that. BACK-2500 --- api/clinic.go | 32 +++---- api/clinicianInvites.go | 95 ++++++++++--------- api/forgot.go | 36 +++---- api/hydrophoneApi.go | 104 ++++++++++++-------- api/hydrophoneApi_test.go | 193 +++++++++++++++++++++++++++++++++++--- api/invite.go | 157 ++++++++++++++++--------------- api/patientInvites.go | 40 ++++---- api/signup.go | 143 +++++++++++++++------------- testutil/testutil.go | 17 ---- testutil/vars.go | 33 +++++++ testutil/zap.go | 88 +++++++++++++++++ 11 files changed, 624 insertions(+), 314 deletions(-) delete mode 100644 testutil/testutil.go create mode 100644 testutil/vars.go create mode 100644 testutil/zap.go diff --git a/api/clinic.go b/api/clinic.go index db98fa6b6..a018af5f5 100644 --- a/api/clinic.go +++ b/api/clinic.go @@ -37,17 +37,17 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ } if permissions, err := a.tokenUserHasRequestedPermissions(token, inviterID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } defer req.Body.Close() var ib = &ClinicInvite{} if err := json.NewDecoder(req.Body).Decode(ib); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -63,11 +63,11 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ Limit: &limit, }) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } if response.JSON200 == nil || len(*response.JSON200) == 0 { - a.sendError(res, http.StatusNotFound, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusNotFound, STATUS_ERR_FINDING_CLINIC, err) return } @@ -76,20 +76,20 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ patientExists, err := a.checkExistingPatientOfClinic(ctx, clinicId, inviterID) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } if patientExists { - a.sendError(res, http.StatusConflict, statusExistingPatientMessage) + a.sendError(ctx, res, http.StatusConflict, statusExistingPatientMessage) return } - existingInvite, err := a.checkForDuplicateClinicInvite(req.Context(), clinicId, inviterID) + existingInvite, err := a.checkForDuplicateClinicInvite(ctx, clinicId, inviterID) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if existingInvite { - a.sendError(res, http.StatusConflict, statusExistingInviteMessage, + a.sendError(ctx, res, http.StatusConflict, statusExistingInviteMessage, zap.String("clinicId", clinicId), zap.String("inviterID", inviterID), err) return } @@ -106,9 +106,9 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ Role: &role, Limit: &maxClinicians, } - listResponse, err := a.clinics.ListCliniciansWithResponse(req.Context(), clinics.ClinicId(clinicId), params) + listResponse, err := a.clinics.ListCliniciansWithResponse(ctx, clinics.ClinicId(clinicId), params) if err != nil || response.StatusCode() != http.StatusOK { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } var recipients []string @@ -120,17 +120,17 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ invite, err := models.NewConfirmationWithContext(models.TypeCareteamInvite, models.TemplateNamePatientClinicInvite, inviterID, ib.Permissions) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } invite.ClinicId = clinicId - if a.addOrUpdateConfirmation(req.Context(), invite, res) { + if a.addOrUpdateConfirmation(ctx, invite, res) { a.logMetric("invite created", req) if err := a.addProfile(invite); err != nil { - a.logger.With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) + a.logger(ctx).With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) return } else if !suppressEmail { fullName := invite.Creator.Profile.FullName @@ -150,7 +150,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ } } - a.sendModelAsResWithStatus(res, invite, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invite, http.StatusOK) return } } diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index 990a15a31..4cd9a3268 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -29,20 +29,20 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va clinic, err := a.clinics.GetClinicWithResponse(ctx, clinics.ClinicId(clinicId)) if err != nil || clinic == nil || clinic.JSON200 == nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } defer req.Body.Close() var body = &ClinicianInvite{} if err := json.NewDecoder(req.Body).Decode(body); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } confirmation, err := models.NewConfirmation(models.TypeClinicianInvite, models.TemplateNameClinicianInvite, token.UserID) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } @@ -51,7 +51,7 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va confirmation.Creator.ClinicId = *clinic.JSON200.Id confirmation.Creator.ClinicName = clinic.JSON200.Name - invitedUsr := a.findExistingUser(body.Email, a.sl.TokenProvide()) + invitedUsr := a.findExistingUser(ctx, body.Email, a.sl.TokenProvide()) if invitedUsr != nil && invitedUsr.UserID != "" { confirmation.UserId = invitedUsr.UserID } @@ -62,7 +62,7 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va Roles: body.Roles, }) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } if response.StatusCode() != http.StatusOK { @@ -74,7 +74,7 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) if code != 0 { - a.sendError(res, code, msg, optionalErr) + a.sendError(ctx, res, code, msg, optionalErr) return } @@ -98,13 +98,13 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, clinic, err := a.clinics.GetClinicWithResponse(ctx, clinics.ClinicId(clinicId)) if err != nil || clinic == nil || clinic.JSON200 == nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } inviteResponse, err := a.clinics.GetInvitedClinicianWithResponse(ctx, clinics.ClinicId(clinicId), clinics.InviteId(inviteId)) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } if inviteResponse.StatusCode() != http.StatusOK || inviteResponse.JSON200 == nil { @@ -119,15 +119,15 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, Type: models.TypeClinicianInvite, Status: models.StatusPending, } - confirmation, err := a.Store.FindConfirmation(req.Context(), filter) + confirmation, err := a.Store.FindConfirmation(ctx, filter) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if confirmation == nil { confirmation, err := models.NewConfirmation(models.TypeClinicianInvite, models.TemplateNameClinicianInvite, token.UserID) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } confirmation.Key = inviteId @@ -138,18 +138,18 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, confirmation.Creator.ClinicId = *clinic.JSON200.Id confirmation.Creator.ClinicName = clinic.JSON200.Name - invitedUsr := a.findExistingUser(confirmation.Email, a.sl.TokenProvide()) + invitedUsr := a.findExistingUser(ctx, confirmation.Email, a.sl.TokenProvide()) if invitedUsr != nil && invitedUsr.UserID != "" { confirmation.UserId = invitedUsr.UserID } code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) if code > 0 { - a.sendError(res, code, msg, optionalErr) + a.sendError(ctx, res, code, msg, optionalErr) return } - a.sendModelAsResWithStatus(res, confirmation, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, confirmation, http.StatusOK) return } } @@ -168,7 +168,7 @@ func (a *Api) GetClinicianInvite(res http.ResponseWriter, req *http.Request, var // Make sure the invite belongs to the clinic inviteResponse, err := a.clinics.GetInvitedClinicianWithResponse(ctx, clinics.ClinicId(clinicId), clinics.InviteId(inviteId)) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CLINIC, err) return } if inviteResponse.StatusCode() != http.StatusOK || inviteResponse.JSON200 == nil { @@ -183,17 +183,17 @@ func (a *Api) GetClinicianInvite(res http.ResponseWriter, req *http.Request, var Type: models.TypeClinicianInvite, Status: models.StatusPending, } - confirmation, err := a.Store.FindConfirmation(req.Context(), filter) + confirmation, err := a.Store.FindConfirmation(ctx, filter) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if confirmation == nil { - a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } - a.sendModelAsResWithStatus(res, confirmation, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, confirmation, http.StatusOK) return } } @@ -204,33 +204,33 @@ func (a *Api) GetClinicianInvitations(res http.ResponseWriter, req *http.Request ctx := req.Context() userId := vars["userId"] - invitedUsr := a.findExistingUser(userId, req.Header.Get(TP_SESSION_TOKEN)) + invitedUsr := a.findExistingUser(ctx, userId, req.Header.Get(TP_SESSION_TOKEN)) // Tokens only legit when for same userid if userId != token.UserID || invitedUsr == nil || invitedUsr.UserID == "" { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeClinicianInvite}, models.StatusPending) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if len(found) == 0 { - a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) return } - if invites := a.addProfileInfoToConfirmations(found); invites != nil { - a.ensureIdSet(req.Context(), userId, invites) + if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { + a.ensureIdSet(ctx, userId, invites) if err := a.populateRestrictions(ctx, *invitedUsr, *token, invites); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } a.logMetric("get_clinician_invitations", req) - a.sendModelAsResWithStatus(res, invites, http.StatusOK) - a.logger.Infof("invites found and checked: %d", len(invites)) + a.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Infof("invites found and checked: %d", len(invites)) return } } @@ -243,11 +243,11 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, userId := vars["userId"] inviteId := vars["inviteId"] - invitedUsr := a.findExistingUser(token.UserID, req.Header.Get(TP_SESSION_TOKEN)) + invitedUsr := a.findExistingUser(ctx, token.UserID, req.Header.Get(TP_SESSION_TOKEN)) // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } @@ -258,32 +258,32 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, Status: models.StatusPending, } - conf, err := a.Store.FindConfirmation(req.Context(), accept) + conf, err := a.Store.FindConfirmation(ctx, accept) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if err := a.populateRestrictions(ctx, *invitedUsr, *token, []*models.Confirmation{conf}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf.Restrictions != nil && !conf.Restrictions.CanAccept { - a.sendError(res, http.StatusForbidden, STATUS_ERR_ACCEPTING_CONFIRMATION) + a.sendError(ctx, res, http.StatusForbidden, STATUS_ERR_ACCEPTING_CONFIRMATION) return } association := clinics.AssociateClinicianToUserJSONRequestBody{UserId: token.UserID} response, err := a.clinics.AssociateClinicianToUserWithResponse(ctx, clinics.ClinicId(conf.ClinicId), clinics.InviteId(inviteId), association) if err != nil || response.StatusCode() != http.StatusOK { - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendModelAsResWithStatus(ctx, res, err, http.StatusInternalServerError) return } conf.UpdateStatus(models.StatusCompleted) - if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) + if !a.addOrUpdateConfirmation(ctx, conf, res) { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } @@ -301,10 +301,10 @@ func (a *Api) DismissClinicianInvite(res http.ResponseWriter, req *http.Request, userId := vars["userId"] inviteId := vars["inviteId"] - invitedUsr := a.findExistingUser(token.UserID, req.Header.Get(TP_SESSION_TOKEN)) + invitedUsr := a.findExistingUser(ctx, token.UserID, req.Header.Get(TP_SESSION_TOKEN)) // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } @@ -316,7 +316,7 @@ func (a *Api) DismissClinicianInvite(res http.ResponseWriter, req *http.Request, } conf, err := a.Store.FindConfirmation(ctx, filter) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { @@ -346,7 +346,7 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, } conf, err := a.Store.FindConfirmation(ctx, filter) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } @@ -356,7 +356,6 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) (code int, msg string, err error) { ctx := req.Context() - if err := a.addProfile(confirmation); err != nil { return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err } @@ -395,7 +394,7 @@ func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http response, err := a.clinics.DeleteInvitedClinicianWithResponse(ctx, clinics.ClinicId(filter.ClinicId), clinics.InviteId(filter.Key)) if err != nil || (response.StatusCode() != http.StatusOK && response.StatusCode() != http.StatusNotFound) { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } @@ -416,10 +415,10 @@ func (a *Api) assertClinicMember(ctx context.Context, clinicId string, token *sh // Non-server tokens only legit when for same userid if !token.IsServer { if result, err := a.clinics.GetClinicianWithResponse(ctx, clinics.ClinicId(clinicId), clinics.ClinicianId(token.UserID)); err != nil || result.StatusCode() == http.StatusInternalServerError { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return err } else if result.StatusCode() != http.StatusOK { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("unexpected status code %v when fetching clinician %v from clinic %v", result.StatusCode(), token.UserID, clinicId) } } @@ -430,10 +429,10 @@ func (a *Api) assertClinicAdmin(ctx context.Context, clinicId string, token *sho // Non-server tokens only legit when for same userid if !token.IsServer { if result, err := a.clinics.GetClinicianWithResponse(ctx, clinics.ClinicId(clinicId), clinics.ClinicianId(token.UserID)); err != nil || result.StatusCode() == http.StatusInternalServerError { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return err } else if result.StatusCode() != http.StatusOK { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("unexpected status code %v when fetching clinician %v from clinic %v", result.StatusCode(), token.UserID, clinicId) } else { clinician := result.JSON200 @@ -442,7 +441,7 @@ func (a *Api) assertClinicAdmin(ctx context.Context, clinicId string, token *sho return nil } } - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return fmt.Errorf("the clinician doesn't have the required permissions %v", clinician.Roles) } } diff --git a/api/forgot.go b/api/forgot.go index 6ba2fc783..7a83b51ad 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -42,7 +42,7 @@ type ( // status: 200 // status: 400 no email given func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map[string]string) { - + ctx := req.Context() email := vars["useremail"] if email == "" { res.WriteHeader(http.StatusBadRequest) @@ -51,28 +51,28 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map resetCnf, err := models.NewConfirmation(models.TypePasswordReset, models.TemplateNamePasswordReset, "") if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } resetCnf.Email = email - if resetUsr := a.findExistingUser(resetCnf.Email, a.sl.TokenProvide()); resetUsr != nil { + if resetUsr := a.findExistingUser(ctx, resetCnf.Email, a.sl.TokenProvide()); resetUsr != nil { resetCnf.UserId = resetUsr.UserID } else { resetCnf, err = models.NewConfirmation(models.TypeNoAccount, models.TemplateNameNoAccount, "") if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } resetCnf.Email = email //there is nothing more to do other than notify the user resetCnf.UpdateStatus(models.StatusCompleted) - a.logger.With(zap.String("email", email)).Info(STATUS_RESET_NO_ACCOUNT) + a.logger(ctx).With(zap.String("email", email)).Info(STATUS_RESET_NO_ACCOUNT) } - if a.addOrUpdateConfirmation(req.Context(), resetCnf, res) { + if a.addOrUpdateConfirmation(ctx, resetCnf, res) { a.logMetricAsServer("reset confirmation created") emailContent := map[string]interface{}{ @@ -92,7 +92,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map // find the reset confirmation if it exists and hasn't expired func (a *Api) findResetConfirmation(ctx context.Context, conf *models.Confirmation) (*models.Confirmation, bool, error) { - a.logger.With("conf", conf).Debug("finding reset confirmation") + a.logger(ctx).With("conf", conf).Debug("finding reset confirmation") found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { return nil, false, err @@ -122,47 +122,47 @@ func (a *Api) findResetConfirmation(ctx context.Context, conf *models.Confirmati // status: 400 STATUS_RESET_ERROR when we can't update the users password // status: 404 STATUS_RESET_NOT_FOUND when no matching reset confirmation is found func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars map[string]string) { - + ctx := req.Context() defer req.Body.Close() var rb = &resetBody{} if err := json.NewDecoder(req.Body).Decode(rb); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } resetCnf := &models.Confirmation{Key: rb.Key, Email: rb.Email, Status: models.StatusPending, Type: models.TypePasswordReset} - conf, expired, err := a.findResetConfirmation(req.Context(), resetCnf) + conf, expired, err := a.findResetConfirmation(ctx, resetCnf) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if expired { - a.sendError(res, http.StatusNotFound, STATUS_RESET_EXPIRED) + a.sendError(ctx, res, http.StatusNotFound, STATUS_RESET_EXPIRED) return } if conf == nil { - a.sendError(res, http.StatusNotFound, STATUS_RESET_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_RESET_NOT_FOUND) return } if resetCnf.Key == "" || resetCnf.Email != conf.Email { - a.sendError(res, http.StatusBadRequest, STATUS_RESET_ERROR) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_RESET_ERROR) return } token := a.sl.TokenProvide() - if usr := a.findExistingUser(rb.Email, token); usr != nil { + if usr := a.findExistingUser(ctx, rb.Email, token); usr != nil { if err := a.sl.UpdateUser(usr.UserID, shoreline.UserUpdate{Password: &rb.Password}, token); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_RESET_ERROR, err, "updating user password") + a.sendError(ctx, res, http.StatusBadRequest, STATUS_RESET_ERROR, err, "updating user password") return } conf.UpdateStatus(models.StatusCompleted) - if a.addOrUpdateConfirmation(req.Context(), conf, res) { + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetricAsServer("password reset") - a.sendOK(res, STATUS_RESET_ACCEPTED) + a.sendOK(ctx, res, STATUS_RESET_ACCEPTED) return } } diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 96c85dec3..5b125eba4 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -7,6 +7,7 @@ import ( "net/http" "reflect" "strings" + "sync" "github.com/gorilla/mux" "github.com/kelseyhightower/envconfig" @@ -35,8 +36,9 @@ type ( seagull commonClients.Seagull metrics highwater.Client alerts AlertsClient - logger *zap.SugaredLogger + baseLogger *zap.SugaredLogger Config Config + mu sync.Mutex } Config struct { ServerSecret string `envconfig:"TIDEPOOL_SERVER_SECRET" required:"true"` @@ -127,7 +129,7 @@ func NewApi( seagull: seagull, alerts: alerts, templates: templates, - logger: logger, + baseLogger: logger, } } @@ -164,24 +166,47 @@ var RouterModule = fx.Options(fx.Provide(routerProvider, apiConfigProvider)) // // This is effected via its type being that of a mux.MiddlewareFunc. func (a *Api) addUserIDToLogger(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) + return http.HandlerFunc(func(w http.ResponseWriter, orig *http.Request) { + vars := mux.Vars(orig) + next := orig for key := range vars { if !strings.EqualFold(key, "userid") { continue } - oldLogger := a.logger - a.logger = a.logger.With(key, vars[key]) - defer func(l *zap.SugaredLogger) { - a.logger = l - }(oldLogger) + ctxLog := a.baseLogger.With(zap.String(key, vars[key])) + ctxWithLog := context.WithValue(orig.Context(), ctxLoggerKey{}, ctxLog) + next = orig.WithContext(ctxWithLog) break } - h.ServeHTTP(w, r) + h.ServeHTTP(w, next) + }) +} + +type ctxLoggerKey struct{} + +func (a *Api) logger(ctx context.Context) *zap.SugaredLogger { + if logger, ok := ctx.Value(ctxLoggerKey{}).(*zap.SugaredLogger); ok { + return logger + } + return a.cloneLogger() +} + +func (a *Api) cloneLogger() *zap.SugaredLogger { + return a.baseLogger.WithOptions() +} + +func (a *Api) ctxLoggerHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origCtx := r.Context() + ctxLog := a.cloneLogger() + ctxWithLog := context.WithValue(origCtx, ctxLoggerKey{}, ctxLog) + rWithLog := r.WithContext(ctxWithLog) + h.ServeHTTP(w, rWithLog) }) } func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { + rtr.Use(mux.MiddlewareFunc(a.ctxLoggerHandler)) c := rtr.PathPrefix("/confirm").Subrouter() c.HandleFunc("/status", a.IsReady).Methods("GET") @@ -303,8 +328,9 @@ func (h varsHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) { } func (a *Api) IsReady(res http.ResponseWriter, req *http.Request) { - if err := a.Store.Ping(req.Context()); err != nil { - a.sendError(res, http.StatusInternalServerError, "store connectivity failure", err) + ctx := req.Context() + if err := a.Store.Ping(ctx); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, "store connectivity failure", err) return } res.WriteHeader(http.StatusOK) @@ -320,7 +346,7 @@ func (a *Api) IsAlive(res http.ResponseWriter, req *http.Request) { // write an error if it all goes wrong func (a *Api) addOrUpdateConfirmation(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) bool { if err := a.Store.UpsertConfirmation(ctx, conf); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return false } return true @@ -339,11 +365,11 @@ func (a *Api) addProfile(conf *models.Confirmation) error { return nil } -func (a *Api) addProfileInfoToConfirmations(results []*models.Confirmation) []*models.Confirmation { +func (a *Api) addProfileInfoToConfirmations(ctx context.Context, results []*models.Confirmation) []*models.Confirmation { for i := range results { if err := a.addProfile(results[i]); err != nil { //report and move on - a.logger.With(zap.Error(err)).Warn("getting profile") + a.logger(ctx).With(zap.Error(err)).Warn("getting profile") } } return results @@ -351,6 +377,7 @@ func (a *Api) addProfileInfoToConfirmations(results []*models.Confirmation) []*m // Generate a notification from the given confirmation,write the error if it fails func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirmation, content map[string]interface{}, recipients ...string) bool { + ctx := req.Context() templateName := conf.TemplateName if templateName == models.TemplateNameUndefined { switch conf.Type { @@ -360,7 +387,7 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma templateName = models.TemplateNameCareteamInvite has, err := conf.HasPermission("follow") if err != nil { - a.logger.With(zap.Error(err)).Warn("permissions check failed; falling back to non-alerting notification") + a.logger(ctx).With(zap.Error(err)).Warn("permissions check failed; falling back to non-alerting notification") } else if has { templateName = models.TemplateNameCareteamInviteWithAlerting } @@ -369,7 +396,7 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma case models.TypeNoAccount: templateName = models.TemplateNameNoAccount default: - a.logger.With(zap.String("type", string(conf.Type))). + a.logger(ctx).With(zap.String("type", string(conf.Type))). Info("unknown confirmation type") return false } @@ -380,14 +407,14 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma template, ok := a.templates[templateName] if !ok { - a.logger.With(zap.String("template", string(templateName))). + a.logger(ctx).With(zap.String("template", string(templateName))). Info("unknown template type") return false } subject, body, err := template.Execute(content) if err != nil { - a.logger.With(zap.Error(err)).Error("executing email template") + a.logger(ctx).With(zap.Error(err)).Error("executing email template") return false } @@ -400,7 +427,7 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma } if status, details := a.notifier.Send(addresses, subject, body); status != http.StatusOK { - a.logger.Errorw( + a.logger(ctx).Errorw( "error sending email", "email", addresses, "subject", subject, @@ -414,18 +441,19 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma // find and validate the token func (a *Api) token(res http.ResponseWriter, req *http.Request) *shoreline.TokenData { + ctx := req.Context() if token := req.Header.Get(TP_SESSION_TOKEN); token != "" { td := a.sl.CheckToken(token) if td == nil { - a.sendError(res, http.StatusForbidden, STATUS_INVALID_TOKEN, + a.sendError(ctx, res, http.StatusForbidden, STATUS_INVALID_TOKEN, zap.String("token", token)) return nil } //all good! return td } - a.sendError(res, http.StatusUnauthorized, STATUS_NO_TOKEN) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_NO_TOKEN) return nil } @@ -445,9 +473,9 @@ func (a *Api) logMetricAsServer(name string) { // Find existing user based on the given indentifier // The indentifier could be either an id or email address -func (a *Api) findExistingUser(indentifier, token string) *shoreline.UserData { +func (a *Api) findExistingUser(ctx context.Context, indentifier, token string) *shoreline.UserData { if usr, err := a.sl.GetUser(indentifier, token); err != nil { - a.logger.With(zap.Error(err)).Error("getting user details") + a.logger(ctx).With(zap.Error(err)).Error("getting user details") return nil } else { return usr @@ -463,10 +491,10 @@ func (a *Api) ensureIdSet(ctx context.Context, userId string, confirmations []*m for i := range confirmations { //set the userid if not set already if confirmations[i].UserId == "" { - a.logger.Debug("UserId wasn't set for invite so setting it") + a.logger(ctx).Debug("UserId wasn't set for invite so setting it") confirmations[i].UserId = userId if err := a.Store.UpsertConfirmation(ctx, confirmations[i]); err != nil { - a.logger.With(zap.Error(err)).Warn("upserting confirmation") + a.logger(ctx).With(zap.Error(err)).Warn("upserting confirmation") } } } @@ -520,9 +548,9 @@ func (a *Api) populateRestrictions(ctx context.Context, user shoreline.UserData, return nil } -func (a *Api) sendModelAsResWithStatus(res http.ResponseWriter, model interface{}, statusCode int) { +func (a *Api) sendModelAsResWithStatus(ctx context.Context, res http.ResponseWriter, model interface{}, statusCode int) { if jsonDetails, err := json.Marshal(model); err != nil { - a.logger.With("model", model, zap.Error(err)).Errorf("trying to send model") + a.logger(ctx).With("model", model, zap.Error(err)).Errorf("trying to send model") http.Error(res, "Error marshaling data for response", http.StatusInternalServerError) } else { res.Header().Set("content-type", "application/json") @@ -531,19 +559,19 @@ func (a *Api) sendModelAsResWithStatus(res http.ResponseWriter, model interface{ } } -func (a *Api) sendError(res http.ResponseWriter, statusCode int, reason string, extras ...interface{}) { - a.sendErrorLog(statusCode, reason, extras...) - a.sendModelAsResWithStatus(res, status.NewStatus(statusCode, reason), statusCode) +func (a *Api) sendError(ctx context.Context, res http.ResponseWriter, statusCode int, reason string, extras ...interface{}) { + a.sendErrorLog(ctx, statusCode, reason, extras...) + a.sendModelAsResWithStatus(ctx, res, status.NewStatus(statusCode, reason), statusCode) } -func (a *Api) sendErrorWithCode(res http.ResponseWriter, statusCode int, errorCode int, reason string, extras ...interface{}) { - a.sendErrorLog(statusCode, reason, extras...) - a.sendModelAsResWithStatus(res, status.NewStatusWithError(statusCode, errorCode, reason), statusCode) +func (a *Api) sendErrorWithCode(ctx context.Context, res http.ResponseWriter, statusCode int, errorCode int, reason string, extras ...interface{}) { + a.sendErrorLog(ctx, statusCode, reason, extras...) + a.sendModelAsResWithStatus(ctx, res, status.NewStatusWithError(statusCode, errorCode, reason), statusCode) } -func (a *Api) sendErrorLog(code int, reason string, extras ...interface{}) { +func (a *Api) sendErrorLog(ctx context.Context, code int, reason string, extras ...interface{}) { nonErrs, errs, fields := splitExtrasAndErrorsAndFields(extras) - log := a.logger.WithOptions(zap.AddCallerSkip(2)). + log := a.logger(ctx).WithOptions(zap.AddCallerSkip(2)). Desugar().With(fields...).Sugar(). With(zap.Int("code", code)). With(zap.Array("extras", zapArrayAny(nonErrs))) @@ -562,8 +590,8 @@ func (a *Api) sendErrorLog(code int, reason string, extras ...interface{}) { } // sendOK helps send a 200 response with a standard form and optional message. -func (a *Api) sendOK(res http.ResponseWriter, reason string) { - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusOK, reason), http.StatusOK) +func (a *Api) sendOK(ctx context.Context, res http.ResponseWriter, reason string) { + a.sendModelAsResWithStatus(ctx, res, status.NewStatus(http.StatusOK, reason), http.StatusOK) } func splitExtrasAndErrorsAndFields(extras []interface{}) ([]interface{}, []error, []zapcore.Field) { diff --git a/api/hydrophoneApi_test.go b/api/hydrophoneApi_test.go index 8bce565e0..28c89ffb8 100644 --- a/api/hydrophoneApi_test.go +++ b/api/hydrophoneApi_test.go @@ -1,13 +1,17 @@ package api import ( + "bytes" "context" "errors" "fmt" "io" "net/http" "net/http/httptest" + "os" "reflect" + "strings" + "sync" "testing" "github.com/golang/mock/gomock" @@ -71,18 +75,23 @@ var ( return NewResponsableMockGatekeeper() })) - BaseModule = fx.Options( - clients.MockNotifierModule, - MockShorelineModule, - MockMetricsModule, - MockSeagullModule, - MockAlertsModule, - MockTemplatesModule, - MockConfigModule, - fx.Provide(testutil.NewLogger), - fx.Provide(NewApi), - fx.Provide(mux.NewRouter), - ) + BaseModuleWithLog = func(rw io.ReadWriter) fx.Option { + return fx.Options( + clients.MockNotifierModule, + MockShorelineModule, + MockMetricsModule, + MockSeagullModule, + MockAlertsModule, + MockTemplatesModule, + MockConfigModule, + fx.Supply(fx.Annotate(rw, fx.As(new(io.ReadWriter)))), + fx.Provide(testutil.NewLoggerWithReadWriter), + fx.Provide(NewApi), + fx.Provide(mux.NewRouter), + ) + } + + BaseModule = BaseModuleWithLog(os.Stderr) ResponableModule = fx.Options( clients.MockStoreFailsModule, @@ -320,6 +329,58 @@ func Test_TokenUserHasRequestedPermissions_FullMatch(t *testing.T) { } } +func TestAddUserIDToLogger(s *testing.T) { + s.Run("is request specific (and thread-safe)", func(t *testing.T) { + // This test is designed to try to exacerbate thread-safety issues in + // Api logging. Unfortunately, it can't 100% reliably produce an error + // in the event of a race condition. + // + // As a result, if this test is flapping, that's a strong indicator + // that there's a thread-safety issue. A symptom of these flaps is + // having the test fail some number of times, then randomly pass, and + // then stay passing. This can be caused by Go caching the test result + // (it's not actually running it again, it just reports the previous + // success). Use the -count=1 flag to go test to force the test to be + // re-run. + userIDs := []string{"foo", "bar", "baz", "quux"} + vars := testutil.WithRotatingVar("userId", userIDs) + ht := newHydrophoneTest(t) + handler := ht.handlerWithSync(len(userIDs)) + + logData := ht.captureLogs(func() { + ts := ht.Server(handler, ht.Api.addUserIDToLogger, vars) + for i := 0; i < len(userIDs); i++ { + go ts.Client().Get(ts.URL) + } + ht.syncer.Sync() + }) + + for _, userID := range userIDs { + expected := fmt.Sprintf(`"userId": "%s"`, userID) + if strings.Count(logData, expected) != 1 { + t.Errorf("expected 1x field %s, got:\n%s", expected, logData) + } + } + }) + + s.Run("includes the userId", func(t *testing.T) { + vars := testutil.WithRotatingVars(map[string]string{"userId": "foo"}) + ht := newHydrophoneTest(t) + + logData := ht.captureLogs(func() { + ts := ht.Server(ht.handlerLog(), ht.Api.addUserIDToLogger, vars) + if _, err := ts.Client().Get(ts.URL); err != nil { + t.Errorf("expected no error, got: %s", err) + } + }) + + expected := `"userId": "foo"` + if !strings.Contains(logData, expected) { + t.Errorf("expected field %s, got: %s", expected, logData) + } + }) +} + type mockAlertsClient struct{} func newMockAlertsClient() *mockAlertsClient { @@ -357,3 +418,111 @@ func MustRequest(t *testing.T, method, url string, body io.Reader) *http.Request } return r } + +// hydrophoneTest bundles useful scaffolding for hydrophone tests. +type hydrophoneTest struct { + *testing.T + Api *Api + logBuf *bytes.Buffer + syncer *syncer +} + +// newHydrophoneTest handles creating the scaffolding for testing a hydrophone +// handler. +func newHydrophoneTest(t *testing.T) *hydrophoneTest { + var api *Api + + logBuf := &bytes.Buffer{} + fx.New( + clients.MockStoreFailsModule, + MockGatekeeperModule, + BaseModuleWithLog(logBuf), + MockClinicsModule, + fx.Supply(t), + fx.Populate(&api), + ) + + return &hydrophoneTest{ + T: t, + Api: api, + logBuf: logBuf, + } +} + +// Server provides a test server, with the provided middleware applied to each +// request. +func (ht *hydrophoneTest) Server(h http.Handler, middleware ...mux.MiddlewareFunc) *httptest.Server { + var combined http.Handler = h + for _, m := range middleware { + combined = m(combined) + } + ts := httptest.NewServer(combined) + ht.Cleanup(ts.Close) + return ts +} + +// handlerOK just responds with a bare 200 header. +func (ht *hydrophoneTest) handlerOK() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) +} + +// handlerLog simply logs "test" +func (ht *hydrophoneTest) handlerLog() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ht.Api.logger(r.Context()).Info("test") + ht.handlerOK().ServeHTTP(w, r) + }) +} + +// handlerWithSync will cause the test server to wait until size requests have +// been made before allowing any of them to finish. +func (ht *hydrophoneTest) handlerWithSync(size int) http.Handler { + ht.syncer = newSyncer(size) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ht.handlerLog().ServeHTTP(w, r) + ht.syncer.Wait() + }) +} + +// captureLogs returns the output written to the logs from within f. +func (ht *hydrophoneTest) captureLogs(f func()) string { + ht.Helper() + prev := ht.logBuf.Len() + f() + return ht.logBuf.String()[prev:] +} + +// syncer synchonizes two goroutines. +// +// It can be useful to produce race conditions. +type syncer struct { + size int + waiting chan struct{} + done chan struct{} + + mu sync.Mutex +} + +func newSyncer(size int) *syncer { + return &syncer{ + size: size, + waiting: make(chan struct{}), + done: make(chan struct{}), + } +} + +func (s *syncer) Wait() { + <-s.waiting + <-s.done +} + +func (s *syncer) Sync() { + s.mu.Lock() + defer s.mu.Unlock() + for i := 0; i < s.size; i++ { + s.waiting <- struct{}{} + } + close(s.done) +} diff --git a/api/invite.go b/api/invite.go index c2e1efc52..3f2b558a6 100644 --- a/api/invite.go +++ b/api/invite.go @@ -49,7 +49,7 @@ func (a *Api) checkForDuplicateInvite(ctx context.Context, inviteeEmail, invitor //rule is we cannot send if the invite is not yet expired if !invites[0].IsExpired() { - a.logger.With(zap.String("email", inviteeEmail)).Debug(statusExistingInviteMessage) + a.logger(ctx).With(zap.String("email", inviteeEmail)).Debug(statusExistingInviteMessage) return true } } @@ -70,13 +70,13 @@ func (a *Api) checkExistingPatientOfClinic(ctx context.Context, clinicId, patien return false, fmt.Errorf("unexpected status code %v when checking if user is existing patient", response.StatusCode()) } -func (a *Api) checkAccountAlreadySharedWithUser(invitorID, inviteeEmail string) (bool, *shoreline.UserData) { +func (a *Api) checkAccountAlreadySharedWithUser(ctx context.Context, invitorID, inviteeEmail string) (bool, *shoreline.UserData) { //already in the group? - invitedUsr := a.findExistingUser(inviteeEmail, a.sl.TokenProvide()) + invitedUsr := a.findExistingUser(ctx, inviteeEmail, a.sl.TokenProvide()) if invitedUsr != nil && invitedUsr.UserID != "" { if perms, err := a.gatekeeper.UserInGroup(invitedUsr.UserID, invitorID); err != nil { - a.logger.With(zap.Error(err)).Error("checking if user is in group") + a.logger(ctx).With(zap.Error(err)).Error("checking if user is in group") } else if perms != nil { return true, invitedUsr } @@ -93,6 +93,7 @@ func (a *Api) checkAccountAlreadySharedWithUser(invitorID, inviteeEmail string) // status: 400 func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { + ctx := req.Context() inviteeID := vars["userid"] if inviteeID == "" { @@ -101,28 +102,28 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, } // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, zap.String("inviteeID", inviteeID)) return } - invitedUsr := a.findExistingUser(inviteeID, req.Header.Get(TP_SESSION_TOKEN)) + invitedUsr := a.findExistingUser(ctx, inviteeID, req.Header.Get(TP_SESSION_TOKEN)) //find all oustanding invites were this user is the invite// - found, err := a.Store.FindConfirmations(req.Context(), &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeCareteamInvite}, models.StatusPending) + found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeCareteamInvite}, models.StatusPending) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if len(found) == 0 { - a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) return } - if invites := a.addProfileInfoToConfirmations(found); invites != nil { - a.ensureIdSet(req.Context(), inviteeID, invites) + if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { + a.ensureIdSet(ctx, inviteeID, invites) a.logMetric("get received invites", req) - a.sendModelAsResWithStatus(res, invites, http.StatusOK) - a.logger.Debugf("invites found and checked: %d", len(invites)) + a.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Debugf("invites found and checked: %d", len(invites)) return } } @@ -136,7 +137,7 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, // status: 400 func (a *Api) GetSentInvitations(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { - + ctx := req.Context() invitorID := vars["userid"] if invitorID == "" { @@ -145,26 +146,26 @@ func (a *Api) GetSentInvitations(res http.ResponseWriter, req *http.Request, var } if permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } //find all invites I have sent that are pending or declined - found, err := a.Store.FindConfirmations(req.Context(), &models.Confirmation{CreatorId: invitorID, Type: models.TypeCareteamInvite}, models.StatusPending, models.StatusDeclined) + found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{CreatorId: invitorID, Type: models.TypeCareteamInvite}, models.StatusPending, models.StatusDeclined) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if len(found) == 0 { - a.sendError(res, http.StatusNotFound, STATUS_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) return } - if invitations := a.addProfileInfoToConfirmations(found); invitations != nil { + if invitations := a.addProfileInfoToConfirmations(ctx, found); invitations != nil { a.logMetric("get sent invites", req) - a.sendModelAsResWithStatus(res, invitations, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invitations, http.StatusOK) return } } @@ -177,13 +178,13 @@ func (a *Api) GetSentInvitations(res http.ResponseWriter, req *http.Request, var // http.StatusForbidden when mismatch of user ID's, type or status func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { - + ctx := req.Context() inviteeID := vars["userid"] invitorID := vars["invitedby"] if inviteeID == "" || invitorID == "" { res.WriteHeader(http.StatusBadRequest) - a.logger. + a.logger(ctx). With(zap.String("inviteeID", inviteeID)). With(zap.String("invitorID", invitorID)). Info("inviteeID or invitorID is not set") @@ -192,29 +193,29 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } accept := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(accept); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } if accept.Key == "" { res.WriteHeader(http.StatusBadRequest) - a.logger.Info("no confirmation key set") + a.logger(ctx).Info("no confirmation key set") return } - conf, err := a.Store.FindConfirmation(req.Context(), accept) + conf, err := a.Store.FindConfirmation(ctx, accept) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { - a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -226,37 +227,37 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ ValidateCreatorID(invitorID, &validationErrors) if len(validationErrors) > 0 { - a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + a.sendError(ctx, res, http.StatusForbidden, statusForbiddenMessage, zap.Errors("validation-errors", validationErrors)) return } ctc := &models.CareTeamContext{} if err := conf.DecodeContext(ctc); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONTEXT) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONTEXT) return } if err := ctc.Validate(); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_VALIDATING_CONTEXT, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_VALIDATING_CONTEXT, err) return } setPerms, err := a.gatekeeper.SetPermissions(inviteeID, invitorID, ctc.Permissions) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SETTING_PERMISSIONS, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SETTING_PERMISSIONS, err) return } - a.logger.With(zapPermsField(setPerms)).Info("permissions set") + a.logger(ctx).With(zapPermsField(setPerms)).Info("permissions set") if ctc.AlertsConfig != nil && ctc.Permissions["follow"] != nil { - if err := a.alerts.Upsert(req.Context(), ctc.AlertsConfig); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_ALERTS_CONFIG, err) + if err := a.alerts.Upsert(ctx, ctc.AlertsConfig); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_ALERTS_CONFIG, err) return } } conf.UpdateStatus(models.StatusCompleted) - if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) + if !a.addOrUpdateConfirmation(ctx, conf, res) { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } a.logMetric("acceptinvite", req) @@ -273,7 +274,7 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ // status: 400 when the incoming data is incomplete or incorrect func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { - + ctx := req.Context() invitorID := vars["userid"] email := vars["invited_address"] @@ -283,10 +284,10 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ } if permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } @@ -297,22 +298,22 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ Type: models.TypeCareteamInvite, } - conf, err := a.Store.FindConfirmation(req.Context(), invite) + conf, err := a.Store.FindConfirmation(ctx, invite) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { //cancel the invite conf.UpdateStatus(models.StatusCanceled) - if a.addOrUpdateConfirmation(req.Context(), conf, res) { + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("canceled invite", req) res.WriteHeader(http.StatusOK) return } } - a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -321,7 +322,7 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ // status: 400 func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { - + ctx := req.Context() inviteeID := vars["userid"] invitorID := vars["invitedby"] @@ -332,13 +333,13 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } dismiss := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(dismiss); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -347,21 +348,21 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map return } - conf, err := a.Store.FindConfirmation(req.Context(), dismiss) + conf, err := a.Store.FindConfirmation(ctx, dismiss) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { conf.UpdateStatus(models.StatusDeclined) - if a.addOrUpdateConfirmation(req.Context(), conf, res) { + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("dismissinvite", req) res.WriteHeader(http.StatusOK) return } } - a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -377,7 +378,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st if token == nil { return } - + ctx := req.Context() invitorID := vars["userid"] if invitorID == "" { res.WriteHeader(http.StatusBadRequest) @@ -390,16 +391,16 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st } permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, requiredPerms) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } var ib = &inviteBody{} if err := json.NewDecoder(req.Body).Decode(ib); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -408,12 +409,12 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st return } - if a.checkForDuplicateInvite(req.Context(), ib.Email, invitorID) { - a.sendError(res, http.StatusConflict, statusExistingInviteMessage, + if a.checkForDuplicateInvite(ctx, ib.Email, invitorID) { + a.sendError(ctx, res, http.StatusConflict, statusExistingInviteMessage, zap.String("email", ib.Email)) return } - alreadyMember, invitedUsr := a.checkAccountAlreadySharedWithUser(invitorID, ib.Email) + alreadyMember, invitedUsr := a.checkAccountAlreadySharedWithUser(ctx, invitorID, ib.Email) if alreadyMember && invitedUsr != nil { // In the past, having an existing relationship would cause this // handler to abort with an error response. With the development of @@ -424,14 +425,14 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st // continue. perms, err := a.gatekeeper.UserInGroup(invitedUsr.UserID, invitorID) if err != nil { - a.sendError(res, http.StatusInternalServerError, statusInternalServerErrorMessage) + a.sendError(ctx, res, http.StatusInternalServerError, statusInternalServerErrorMessage) return } if !addsAlertingPermissions(perms, ib.Permissions) { // Since this invitation doesn't add alerting permissions, // maintain the previous handler's behavior, and abort with an // error response. - a.sendError(res, http.StatusConflict, statusExistingMemberMessage, + a.sendError(ctx, res, http.StatusConflict, statusExistingMemberMessage, zap.String("email", ib.Email), zap.String("invitorID", invitorID)) return } @@ -439,7 +440,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st for key := range perms { ib.Permissions[key] = perms[key] } - a.logger.With(zapPermsField(perms)).Info("permissions set") + a.logger(ctx).With(zapPermsField(perms)).Info("permissions set") } templateName := models.TemplateNameCareteamInvite @@ -449,7 +450,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st invite, err := models.NewConfirmationWithContext(models.TypeCareteamInvite, templateName, invitorID, ib.CareTeamContext) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } @@ -458,14 +459,14 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st invite.UserId = invitedUsr.UserID } - if !a.addOrUpdateConfirmation(req.Context(), invite, res) { + if !a.addOrUpdateConfirmation(ctx, invite, res) { return } a.logMetric("invite created", req) if err := a.addProfile(invite); err != nil { - a.logger.With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) - a.sendModelAsResWithStatus(res, invite, http.StatusOK) + a.logger(ctx).With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) + a.sendModelAsResWithStatus(ctx, res, invite, http.StatusOK) } fullName := "Tidepool User" @@ -493,7 +494,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st a.logMetric("invite sent", req) } - a.sendModelAsResWithStatus(res, invite, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invite, http.StatusOK) return } @@ -505,7 +506,7 @@ func addsAlertingPermissions(existingPerms, newPerms commonClients.Permissions) func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { inviteId := vars["inviteId"] - + ctx := req.Context() if inviteId == "" { res.WriteHeader(http.StatusBadRequest) return @@ -517,35 +518,35 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ Type: models.TypeCareteamInvite, } - invite, err := a.Store.FindConfirmation(req.Context(), find) + invite, err := a.Store.FindConfirmation(ctx, find) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if invite == nil || invite.ClinicId != "" { if invite.ClinicId != "" { - a.logger.Info("cannot resend clinic invite using care team invite endpoint") + a.logger(ctx).Info("cannot resend clinic invite using care team invite endpoint") } else { - a.logger.Info("cannot resend confirmation, because it doesn't exist") + a.logger(ctx).Info("cannot resend confirmation, because it doesn't exist") } - a.sendError(res, http.StatusForbidden, statusForbiddenMessage) + a.sendError(ctx, res, http.StatusForbidden, statusForbiddenMessage) return } if permissions, err := a.tokenUserHasRequestedPermissions(token, invite.CreatorId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusForbidden, statusForbiddenMessage) + a.sendError(ctx, res, http.StatusForbidden, statusForbiddenMessage) return } invite.ResetCreationAttributes() - if a.addOrUpdateConfirmation(req.Context(), invite, res) { + if a.addOrUpdateConfirmation(ctx, invite, res) { a.logMetric("invite updated", req) if err := a.addProfile(invite); err != nil { - a.logger.With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) + a.logger(ctx).With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) } else { fullName := invite.Creator.Profile.FullName if invite.Creator.Profile.Patient.IsOtherPerson { @@ -568,7 +569,7 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ } } - a.sendModelAsResWithStatus(res, invite, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invite, http.StatusOK) return } } diff --git a/api/patientInvites.go b/api/patientInvites.go index 9a5351a5e..350a9b802 100644 --- a/api/patientInvites.go +++ b/api/patientInvites.go @@ -30,18 +30,18 @@ func (a *Api) GetPatientInvites(res http.ResponseWriter, req *http.Request, vars // find all outstanding invites that are associated to this clinic found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{ClinicId: clinicId, Type: models.TypeCareteamInvite}, models.StatusPending) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if len(found) == 0 { result := make([]*models.Confirmation, 0) - a.sendModelAsResWithStatus(res, result, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, result, http.StatusOK) return } - if invites := a.addProfileInfoToConfirmations(found); invites != nil { + if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { a.logMetric("get_patient_invites", req) - a.sendModelAsResWithStatus(res, invites, http.StatusOK) - a.logger.Debugf("confirmations found and checked: %d", len(invites)) + a.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Debugf("confirmations found and checked: %d", len(invites)) return } } @@ -68,13 +68,13 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va Key: inviteId, } - conf, err := a.Store.FindConfirmation(req.Context(), accept) + conf, err := a.Store.FindConfirmation(ctx, accept) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { - a.sendError(res, http.StatusNotFound, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -84,25 +84,25 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va conf.ValidateClinicID(clinicId, &validationErrors) if len(validationErrors) > 0 { - a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + a.sendError(ctx, res, http.StatusForbidden, statusForbiddenMessage, zap.Errors("validation-errors", validationErrors)) return } patient, err := a.createClinicPatient(ctx, *conf) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_PATIENT, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_PATIENT, err) return } conf.UpdateStatus(models.StatusCompleted) - if !a.addOrUpdateConfirmation(req.Context(), conf, res) { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) + if !a.addOrUpdateConfirmation(ctx, conf, res) { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } a.logMetric("accept_patient_invite", req) - a.sendModelAsResWithStatus(res, patient, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, patient, http.StatusOK) return } } @@ -124,13 +124,13 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re Key: inviteId, } - conf, err := a.Store.FindConfirmation(req.Context(), accept) + conf, err := a.Store.FindConfirmation(ctx, accept) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf == nil { - a.sendError(res, http.StatusForbidden, statusInviteNotFoundMessage) + a.sendError(ctx, res, http.StatusForbidden, statusInviteNotFoundMessage) return } @@ -148,13 +148,13 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re conf.ValidateClinicID(clinicId, &validationErrors) if len(validationErrors) > 0 { - a.sendError(res, http.StatusForbidden, statusForbiddenMessage, + a.sendError(ctx, res, http.StatusForbidden, statusForbiddenMessage, zap.Errors("validation-errors", validationErrors)) return } conf.UpdateStatus(updatedStatus) - if !a.addOrUpdateConfirmation(req.Context(), conf, res) { + if !a.addOrUpdateConfirmation(ctx, conf, res) { return } @@ -164,7 +164,7 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re a.logMetric("cancel_clinic_invite", req) } - a.sendModelAsResWithStatus(res, conf, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, conf, http.StatusOK) return } } @@ -204,7 +204,7 @@ func (a *Api) createClinicPatient(ctx context.Context, confirmation models.Confi patient = response.JSON200 } - a.logger.With(zap.Any("perms", patient.Permissions)).Info("permissions set") + a.logger(ctx).With(zap.Any("perms", patient.Permissions)).Info("permissions set") return patient, nil } diff --git a/api/signup.go b/api/signup.go index a998b2d27..df43c71d6 100644 --- a/api/signup.go +++ b/api/signup.go @@ -22,11 +22,11 @@ import ( func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res http.ResponseWriter) *models.Confirmation { found, err := a.Store.FindConfirmation(ctx, conf) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } if found == nil { - a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return nil } @@ -35,33 +35,34 @@ func (a *Api) findSignUp(ctx context.Context, conf *models.Confirmation, res htt // update an existing signup confirmation func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.ResponseWriter, req *http.Request) { + ctx := req.Context() fromBody := &models.Confirmation{} if err := json.NewDecoder(req.Body).Decode(fromBody); err != nil { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } if fromBody.Key == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) return } - found, err := a.Store.FindConfirmation(req.Context(), fromBody) + found, err := a.Store.FindConfirmation(ctx, fromBody) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if found != nil { updatedStatus := string(newStatus) + " signup" found.UpdateStatus(newStatus) - if a.addOrUpdateConfirmation(req.Context(), found, res) { + if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer(updatedStatus) res.WriteHeader(http.StatusOK) return } } else { - a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return } } @@ -78,14 +79,15 @@ func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.Respons // status: 403 STATUS_EXISTING_SIGNUP // status: 500 STATUS_ERR_FINDING_USER func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() if newSignUp := a.upsertSignUp(res, req, vars); newSignUp != nil { clinicName := "Diabetes Clinic" creatorName := "Clinician" if newSignUp.ClinicId != "" { - resp, err := a.clinics.GetClinicWithResponse(req.Context(), clinics.ClinicId(newSignUp.ClinicId)) + resp, err := a.clinics.GetClinicWithResponse(ctx, clinics.ClinicId(newSignUp.ClinicId)) if err != nil { - a.sendError(res, http.StatusInternalServerError, "unable to fetch clinic") + a.sendError(ctx, res, http.StatusInternalServerError, "unable to fetch clinic") return } if resp.StatusCode() == http.StatusOK && resp.JSON200.Name != "" { @@ -94,7 +96,7 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st } if err := a.addProfile(newSignUp); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) return } if newSignUp.Creator.Profile != nil && newSignUp.Creator.Profile.FullName != "" { @@ -103,11 +105,11 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st profile := &models.Profile{} if err := a.seagull.GetCollection(newSignUp.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting user profile", err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting user profile", err) return } - a.logger. + a.logger(ctx). With(zap.String("email", newSignUp.Email)). With(zap.String("key", newSignUp.Key)). Debug("sending email confirmation") @@ -138,35 +140,36 @@ func (a *Api) sendSignUp(res http.ResponseWriter, req *http.Request, vars map[st // status: 200 // status: 404 STATUS_SIGNUP_EXPIRED func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() email := vars["useremail"] toFind := &models.Confirmation{Email: email, Status: models.StatusPending, Type: models.TypeSignUp} - if found := a.findSignUp(req.Context(), toFind, res); found != nil { - if err := a.Store.RemoveConfirmation(req.Context(), found); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) + if found := a.findSignUp(ctx, toFind, res); found != nil { + if err := a.Store.RemoveConfirmation(ctx, found); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return } if err := found.ResetKey(); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return } - if a.addOrUpdateConfirmation(req.Context(), found, res) { + if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer("signup confirmation recreated") if err := a.addProfile(found); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) return } else { profile := &models.Profile{} if err := a.seagull.GetCollection(found.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } - a.logger. + a.logger(ctx). With(zap.String("email", found.Email)). With(zap.String("key", found.Key)). Debug("resending email confirmation") @@ -182,9 +185,9 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ } if found.ClinicId != "" { - resp, err := a.clinics.GetClinicWithResponse(req.Context(), clinics.ClinicId(found.ClinicId)) + resp, err := a.clinics.GetClinicWithResponse(ctx, clinics.ClinicId(found.ClinicId)) if err != nil { - a.sendError(res, http.StatusInternalServerError, "unable to fetch clinic") + a.sendError(ctx, res, http.StatusInternalServerError, "unable to fetch clinic") return } if resp.StatusCode() == http.StatusOK && resp.JSON200.Name != "" { @@ -217,18 +220,19 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ // status: 400 STATUS_INVALID_BIRTHDAY // status: 400 STATUS_MISMATCH_BIRTHDAY func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() confirmationId := vars["confirmationid"] if confirmationId == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) return } toFind := &models.Confirmation{Key: confirmationId} - if found := a.findSignUp(req.Context(), toFind, res); found != nil { + if found := a.findSignUp(ctx, toFind, res); found != nil { if found.IsExpired() { - a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_EXPIRED) + a.sendError(ctx, res, http.StatusNotFound, STATUS_SIGNUP_EXPIRED) return } @@ -236,43 +240,43 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ updates := shoreline.UserUpdate{EmailVerified: &emailVerified} if user, err := a.sl.GetUser(found.UserId, a.sl.TokenProvide()); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "trying to get user to check email verified", err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "trying to get user to check email verified", err) return } else if !user.PasswordExists { acceptance := &models.Acceptance{} if req.Body != nil { if err := json.NewDecoder(req.Body).Decode(acceptance); err != nil { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_NO_PASSWORD, STATUS_NO_PASSWORD, "decoding acceptance", err) + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_NO_PASSWORD, STATUS_NO_PASSWORD, "decoding acceptance", err) return } } if acceptance.Password == "" { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_PASSWORD, STATUS_MISSING_PASSWORD, "missing password") + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_MISSING_PASSWORD, STATUS_MISSING_PASSWORD, "missing password") return } if !IsValidPassword(acceptance.Password) { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_PASSWORD, STATUS_INVALID_PASSWORD, "invalid password specified") + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_INVALID_PASSWORD, STATUS_INVALID_PASSWORD, "invalid password specified") return } if acceptance.Birthday == "" { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISSING_BIRTHDAY, STATUS_MISSING_BIRTHDAY, "missing birthday") + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_MISSING_BIRTHDAY, STATUS_MISSING_BIRTHDAY, "missing birthday") return } if !IsValidDate(acceptance.Birthday) { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_INVALID_BIRTHDAY, STATUS_INVALID_BIRTHDAY, "invalid birthday specified") + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_INVALID_BIRTHDAY, STATUS_INVALID_BIRTHDAY, "invalid birthday specified") return } profile := &models.Profile{} if err := a.seagull.GetCollection(found.UserId, "profile", a.sl.TokenProvide(), profile); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting the users profile", err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, "getting the users profile", err) return } if acceptance.Birthday != profile.Patient.Birthday { - a.sendErrorWithCode(res, http.StatusConflict, ERROR_MISMATCH_BIRTHDAY, STATUS_MISMATCH_BIRTHDAY, "acceptance birthday does not match user patient birthday") + a.sendErrorWithCode(ctx, res, http.StatusConflict, ERROR_MISMATCH_BIRTHDAY, STATUS_MISMATCH_BIRTHDAY, "acceptance birthday does not match user patient birthday") return } @@ -280,12 +284,12 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ } if err := a.sl.UpdateUser(found.UserId, updates, a.sl.TokenProvide()); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_UPDATING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_UPDATING_USER, err) return } found.UpdateStatus(models.StatusCompleted) - if a.addOrUpdateConfirmation(req.Context(), found, res) { + if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer("accept signup") } @@ -303,13 +307,14 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ // status: 400 STATUS_ERR_DECODING_CONFIRMATION // status: 404 STATUS_SIGNUP_NOT_FOUND func (a *Api) dismissSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() userId := vars["userid"] if userId == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } - a.logger.Debug("dismissing invite") + a.logger(ctx).Debug("dismissing invite") a.updateSignupConfirmation(models.StatusDeclined, res, req) } @@ -318,36 +323,37 @@ func (a *Api) dismissSignUp(res http.ResponseWriter, req *http.Request, vars map // status: 200 with a single result in an array // status: 404 func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() if token := a.token(res, req); token != nil { userId := vars["userid"] if userId == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } if permissions, err := a.tokenUserHasRequestedPermissions(token, userId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return } - signup, err := a.Store.FindConfirmation(req.Context(), &models.Confirmation{UserId: userId, Type: models.TypeSignUp, Status: models.StatusPending}) + signup, err := a.Store.FindConfirmation(ctx, &models.Confirmation{UserId: userId, Type: models.TypeSignUp, Status: models.StatusPending}) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if signup == nil { - a.sendError(res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) + a.sendError(ctx, res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return } else { a.logMetric("get signup", req) - a.sendModelAsResWithStatus(res, signup, http.StatusOK) - a.logger.Debug("found a pending signup") + a.sendModelAsResWithStatus(ctx, res, signup, http.StatusOK) + a.logger(ctx).Debug("found a pending signup") return } } @@ -361,8 +367,9 @@ func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[str // status: 403 STATUS_EXISTING_SIGNUP // status: 500 STATUS_ERR_FINDING_USER func (a *Api) createSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() if newSignUp := a.upsertSignUp(res, req, vars); newSignUp != nil { - a.sendModelAsResWithStatus(res, newSignUp, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, newSignUp, http.StatusOK) } } @@ -375,40 +382,41 @@ func (a *Api) createSignUp(res http.ResponseWriter, req *http.Request, vars map[ // status: 403 STATUS_EXISTING_SIGNUP // status: 500 STATUS_ERR_FINDING_USER func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) *models.Confirmation { + ctx := req.Context() if token := a.token(res, req); token != nil { userId := vars["userid"] if userId == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return nil } if permissions, err := a.tokenUserHasRequestedPermissions(token, userId, commonClients.Permissions{"root": commonClients.Allowed, "custodian": commonClients.Allowed}); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return nil } else if permissions["root"] == nil && permissions["custodian"] == nil { - a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) return nil } var upsertCustodialSignUpInvite UpsertCustodialSignUpInvite if err := json.NewDecoder(req.Body).Decode(&upsertCustodialSignUpInvite); err != nil && err != io.EOF { - a.sendError(res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION) return nil } if usrDetails, err := a.sl.GetUser(userId, a.sl.TokenProvide()); err != nil { - a.sendError(res, http.StatusNotFound, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusNotFound, STATUS_ERR_FINDING_USER, err) return nil } else if len(usrDetails.Emails) == 0 { // Delete existing any existing invites if the email address is empty - existing, err := a.Store.FindConfirmation(req.Context(), &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) + existing, err := a.Store.FindConfirmation(ctx, &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } if existing != nil { - if err := a.Store.RemoveConfirmation(req.Context(), existing); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) + if err := a.Store.RemoveConfirmation(ctx, existing); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return nil } } @@ -417,9 +425,9 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ } else { // get any existing confirmations - newSignUp, err := a.Store.FindConfirmation(req.Context(), &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) + newSignUp, err := a.Store.FindConfirmation(ctx, &models.Confirmation{UserId: usrDetails.UserID, Type: models.TypeSignUp}) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } else if newSignUp == nil { @@ -441,7 +449,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ } else { tokenUserDetails, err := a.sl.GetUser(token.UserID, a.sl.TokenProvide()) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return nil } @@ -459,7 +467,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ newSignUp, err = models.NewConfirmation(models.TypeSignUp, templateName, creatorID) if err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return nil } @@ -468,13 +476,13 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ newSignUp.ClinicId = clinicId } else if newSignUp.Email != usrDetails.Emails[0] { - if err := a.Store.RemoveConfirmation(req.Context(), newSignUp); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) + if err := a.Store.RemoveConfirmation(ctx, newSignUp); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return nil } if err := newSignUp.ResetKey(); err != nil { - a.sendError(res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return nil } @@ -484,11 +492,11 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ newSignUp.CreatorId = upsertCustodialSignUpInvite.InvitedBy } } else { - a.sendError(res, http.StatusForbidden, STATUS_EXISTING_SIGNUP) + a.sendError(ctx, res, http.StatusForbidden, STATUS_EXISTING_SIGNUP) return nil } - if a.addOrUpdateConfirmation(req.Context(), newSignUp, res) { + if a.addOrUpdateConfirmation(ctx, newSignUp, res) { a.logMetric("signup confirmation created", req) return newSignUp @@ -501,14 +509,15 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ // status: 200 // status: 400 STATUS_SIGNUP_NO_ID func (a *Api) cancelSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { + ctx := req.Context() userId := vars["userid"] if userId == "" { - a.sendError(res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } a.updateSignupConfirmation(models.StatusCanceled, res, req) - a.logger.Debug("canceled signup") + a.logger(ctx).Debug("canceled signup") } func IsValidPassword(password string) bool { diff --git a/testutil/testutil.go b/testutil/testutil.go deleted file mode 100644 index 77a935b20..000000000 --- a/testutil/testutil.go +++ /dev/null @@ -1,17 +0,0 @@ -package testutil - -import ( - "testing" - - "go.uber.org/zap" -) - -func NewLogger(t *testing.T) *zap.SugaredLogger { - config := zap.NewDevelopmentConfig() - logger, err := config.Build() - if err != nil { - t.Logf("error configuring zap logger: %s", err) - logger = zap.NewNop() - } - return logger.Sugar() -} diff --git a/testutil/vars.go b/testutil/vars.go new file mode 100644 index 000000000..62a5d3bdf --- /dev/null +++ b/testutil/vars.go @@ -0,0 +1,33 @@ +package testutil + +import ( + "net/http" + "sync/atomic" + + "github.com/gorilla/mux" +) + +// WithRotatingVar provides a middleware to set variables on HTTP requests. +// +// It allows for bypassing a mux.Router with path parameters. +func WithRotatingVar(key string, ids []string) mux.MiddlewareFunc { + maps := []map[string]string{} + for _, id := range ids { + maps = append(maps, map[string]string{key: id}) + } + return WithRotatingVars(maps...) +} + +// WithRotatingVars provides middleware to set variables on HTTP requests. +// +// The sets of vars are rotated through with each request. An atomic int is +// used to ensure thread-safety. +func WithRotatingVars(vars ...map[string]string) mux.MiddlewareFunc { + i := &atomic.Int32{} + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + v := vars[int(i.Add(1))%len(vars)] + h.ServeHTTP(w, mux.SetURLVars(r, v)) + }) + } +} diff --git a/testutil/zap.go b/testutil/zap.go new file mode 100644 index 000000000..f27230cbd --- /dev/null +++ b/testutil/zap.go @@ -0,0 +1,88 @@ +package testutil + +import ( + "fmt" + "io" + "net/url" + "sync/atomic" + "testing" + + "go.uber.org/zap" +) + +func NewLogger(t *testing.T) *zap.SugaredLogger { + return NewLoggerWithReadWriter(t, newNullReadWriter()) +} + +// newNullReadWriter is for zap logging to /dev/null. +func newNullReadWriter() *nullReadWriter { + return &nullReadWriter{Writer: io.Discard} +} + +type nullReadWriter struct{ io.Writer } + +func (r nullReadWriter) Read(b []byte) (int, error) { + return 0, nil +} + +// NewLoggerWithReadWriter provides a zap logger from an io.ReadWriter. +// +// Using a ReadWriter is easier than having to deal with files on disk. +func NewLoggerWithReadWriter(t *testing.T, rw io.ReadWriter) *zap.SugaredLogger { + // Zap doesn't provide a mechanism for using a bare io.Writer as a log. :( + // But they do allow the registration of a scheme and factory pair. With + // the generation of a unique scheme, a test log can be built that uses an + // io.Writer. The scheme has to be unique, because zap won't allow the + // registration of a scheme twice. + scheme := TestScheme(t) + factory := func(u *url.URL) (zap.Sink, error) { return newTestZapSink(rw), nil } + if err := zap.RegisterSink(scheme, factory); err != nil { + t.Fatalf("registering zap scheme %q: %s", scheme, err) + } + cfg := zap.NewDevelopmentConfig() + cfg.OutputPaths = []string{scheme + "://" + t.Name()} + base, err := cfg.Build() + if err != nil { + t.Fatalf("building zap logger: %s", err) + } + return base.Sugar() +} + +var schemeIndex = &atomic.Int64{} + +// TestScheme generates a scheme that's unique to the test. +// +// It relies on testing.T.Name providing a unique name (which it should). +func TestScheme(t *testing.T) string { + // schemes must start with [a-zA-Z] + return fmt.Sprintf("test%d", schemeIndex.Add(1)) +} + +// testZapSink adapts an io.Writer to function as a zap.Sink. +// +// Using a io.Writer allows us to skip needing to write logs to disk, which is +// handy for testing. +type testZapSink struct { + io.Writer +} + +func newTestZapSink(w io.Writer) *testZapSink { + return &testZapSink{ + Writer: w, + } +} + +// Write implements zap.Sink +func (s *testZapSink) Write(p []byte) (n int, err error) { + return s.Writer.Write(p) +} + +// Sync implements zap.Sink +func (s *testZapSink) Sync() error { + return nil +} + +// Close implements zap.Sink +func (s *testZapSink) Close() error { + return nil +} From ce60c2f15e14240e701935000a44fead0779e586 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Wed, 29 Nov 2023 14:22:51 -0700 Subject: [PATCH 06/10] refine and restore logging context From a previous code review it was pointed out that some amount of context was lost from some log messages. I have reviewed all the logging changes, and found these places, and restored them. I may have missed a few, but I made a best-effort attempt. BACK-2500 --- api/clinic.go | 11 +- api/clinicianInvites.go | 27 ++- api/forgot.go | 8 +- api/hydrophoneApi.go | 2 +- api/invite.go | 19 +- api/patientInvites.go | 5 + api/signup.go | 5 + go.work.sum | 424 +++++++++++++++++++++++++++++++++++++++- hydrophone.go | 2 +- 9 files changed, 477 insertions(+), 26 deletions(-) diff --git a/api/clinic.go b/api/clinic.go index a018af5f5..678787434 100644 --- a/api/clinic.go +++ b/api/clinic.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" "go.uber.org/zap" @@ -76,16 +77,19 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ patientExists, err := a.checkExistingPatientOfClinic(ctx, clinicId, inviterID) if err != nil { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err, + "checking if user is already a patient of clinic") return } if patientExists { - a.sendError(ctx, res, http.StatusConflict, statusExistingPatientMessage) + a.sendError(ctx, res, http.StatusConflict, statusExistingPatientMessage, + "user is already a patient of clinic") return } existingInvite, err := a.checkForDuplicateClinicInvite(ctx, clinicId, inviterID) if err != nil { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + fmt.Sprintf("clinic %s user already has or had an invite from %v", clinicId, inviterID)) return } if existingInvite { @@ -126,6 +130,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ invite.ClinicId = clinicId + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, invite, res) { a.logMetric("invite created", req) diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index 4cd9a3268..9c2ad3f94 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -7,6 +7,8 @@ import ( "net/http" "time" + "go.uber.org/zap" + clinics "github.com/tidepool-org/clinic/client" "github.com/tidepool-org/go-common/clients/shoreline" "github.com/tidepool-org/hydrophone/models" @@ -24,6 +26,7 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va clinicId := vars["clinicId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { + // assertClinicAdmin will log and send a response return } @@ -93,6 +96,7 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { + // assertClinicAdmin will log and send a response return } @@ -144,7 +148,7 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, } code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) - if code > 0 { + if code != 0 { a.sendError(ctx, res, code, msg, optionalErr) return } @@ -162,6 +166,7 @@ func (a *Api) GetClinicianInvite(res http.ResponseWriter, req *http.Request, var inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { + // assertClinicAdmin will log and send a response return } @@ -208,7 +213,8 @@ func (a *Api) GetClinicianInvitations(res http.ResponseWriter, req *http.Request // Tokens only legit when for same userid if userId != token.UserID || invitedUsr == nil || invitedUsr.UserID == "" { - a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") return } @@ -224,7 +230,8 @@ func (a *Api) GetClinicianInvitations(res http.ResponseWriter, req *http.Request if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { a.ensureIdSet(ctx, userId, invites) if err := a.populateRestrictions(ctx, *invitedUsr, *token, invites); err != nil { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "error populating restriction in invites for user") return } @@ -247,7 +254,8 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") return } @@ -265,7 +273,8 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, } if err := a.populateRestrictions(ctx, *invitedUsr, *token, []*models.Confirmation{conf}); err != nil { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "error populating restriction in invites for uiser") return } @@ -282,8 +291,8 @@ func (a *Api) AcceptClinicianInvite(res http.ResponseWriter, req *http.Request, } conf.UpdateStatus(models.StatusCompleted) + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } @@ -304,7 +313,8 @@ func (a *Api) DismissClinicianInvite(res http.ResponseWriter, req *http.Request, invitedUsr := a.findExistingUser(ctx, token.UserID, req.Header.Get(TP_SESSION_TOKEN)) // Tokens only legit when for same userid if token.IsServer || userId != token.UserID || invitedUsr == nil || invitedUsr.UserID != token.UserID { - a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") return } @@ -335,6 +345,7 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, inviteId := vars["inviteId"] if err := a.assertClinicAdmin(ctx, clinicId, token, res); err != nil { + // assertClinicAdmin will log and send a response return } @@ -357,6 +368,7 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) (code int, msg string, err error) { ctx := req.Context() if err := a.addProfile(confirmation); err != nil { + a.logger(ctx).With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err } @@ -400,6 +412,7 @@ func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http if conf != nil { conf.UpdateStatus(statusUpdate) + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { return } diff --git a/api/forgot.go b/api/forgot.go index 7a83b51ad..d000d5196 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -60,6 +60,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map if resetUsr := a.findExistingUser(ctx, resetCnf.Email, a.sl.TokenProvide()); resetUsr != nil { resetCnf.UserId = resetUsr.UserID } else { + a.logger(ctx).With(zap.String("email", email)).Debug(STATUS_RESET_NO_ACCOUNT) resetCnf, err = models.NewConfirmation(models.TypeNoAccount, models.TemplateNameNoAccount, "") if err != nil { a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) @@ -72,6 +73,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map a.logger(ctx).With(zap.String("email", email)).Info(STATUS_RESET_NO_ACCOUNT) } + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, resetCnf, res) { a.logMetricAsServer("reset confirmation created") @@ -126,7 +128,8 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma defer req.Body.Close() var rb = &resetBody{} if err := json.NewDecoder(req.Body).Decode(rb); err != nil { - a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err, + "acceptPassword: error decoding reset details") return } @@ -160,7 +163,8 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma return } conf.UpdateStatus(models.StatusCompleted) - if a.addOrUpdateConfirmation(ctx, conf, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetricAsServer("password reset") a.sendOK(ctx, res, STATUS_RESET_ACCEPTED) return diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 5b125eba4..0f0ccb055 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -475,7 +475,7 @@ func (a *Api) logMetricAsServer(name string) { // The indentifier could be either an id or email address func (a *Api) findExistingUser(ctx context.Context, indentifier, token string) *shoreline.UserData { if usr, err := a.sl.GetUser(indentifier, token); err != nil { - a.logger(ctx).With(zap.Error(err)).Error("getting user details") + a.logger(ctx).With(zap.Error(err)).Error("getting existing user details") return nil } else { return usr diff --git a/api/invite.go b/api/invite.go index 3f2b558a6..6473f4ef4 100644 --- a/api/invite.go +++ b/api/invite.go @@ -49,7 +49,9 @@ func (a *Api) checkForDuplicateInvite(ctx context.Context, inviteeEmail, invitor //rule is we cannot send if the invite is not yet expired if !invites[0].IsExpired() { - a.logger(ctx).With(zap.String("email", inviteeEmail)).Debug(statusExistingInviteMessage) + a.logger(ctx).With(zap.String("email", inviteeEmail), + zap.String("extra", "last invite not yet expired")). + Debug(statusExistingInviteMessage) return true } } @@ -102,8 +104,8 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, } // Non-server tokens only legit when for same userid if !token.IsServer && inviteeID != token.UserID { - a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, - zap.String("inviteeID", inviteeID)) + extra := fmt.Sprintf("token owner %s is not authorized to accept invite of for %s", token.UserID, inviteeID) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, zap.String("inviteeID", inviteeID), extra) return } @@ -112,7 +114,8 @@ func (a *Api) GetReceivedInvitations(res http.ResponseWriter, req *http.Request, //find all oustanding invites were this user is the invite// found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{Email: invitedUsr.Emails[0], Type: models.TypeCareteamInvite}, models.StatusPending) if err != nil { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "while finding pending invites") return } if len(found) == 0 { @@ -256,8 +259,8 @@ func (a *Api) AcceptInvite(res http.ResponseWriter, req *http.Request, vars map[ } } conf.UpdateStatus(models.StatusCompleted) + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } a.logMetric("acceptinvite", req) @@ -306,7 +309,7 @@ func (a *Api) CancelInvite(res http.ResponseWriter, req *http.Request, vars map[ if conf != nil { //cancel the invite conf.UpdateStatus(models.StatusCanceled) - + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("canceled invite", req) res.WriteHeader(http.StatusOK) @@ -355,7 +358,7 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map } if conf != nil { conf.UpdateStatus(models.StatusDeclined) - + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("dismissinvite", req) res.WriteHeader(http.StatusOK) @@ -459,6 +462,7 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st invite.UserId = invitedUsr.UserID } + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, invite, res) { return } @@ -542,6 +546,7 @@ func (a *Api) ResendInvite(res http.ResponseWriter, req *http.Request, vars map[ } invite.ResetCreationAttributes() + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, invite, res) { a.logMetric("invite updated", req) diff --git a/api/patientInvites.go b/api/patientInvites.go index 350a9b802..5287df825 100644 --- a/api/patientInvites.go +++ b/api/patientInvites.go @@ -23,6 +23,7 @@ func (a *Api) GetPatientInvites(res http.ResponseWriter, req *http.Request, vars return } + // assertClinicMember logs and writes a response on errors if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { return } @@ -59,6 +60,7 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va return } + // addOrUpdateConfirmation logs and writes a response on errors if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { return } @@ -96,6 +98,7 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va } conf.UpdateStatus(models.StatusCompleted) + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return @@ -137,6 +140,7 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re updatedStatus := models.StatusCanceled if token.UserID != conf.CreatorId { updatedStatus = models.StatusDeclined + // assertClinicMember logs and writes a response on errors if err := a.assertClinicMember(ctx, clinicId, token, res); err != nil { return } @@ -154,6 +158,7 @@ func (a *Api) CancelOrDismissPatientInvite(res http.ResponseWriter, req *http.Re } conf.UpdateStatus(updatedStatus) + // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { return } diff --git a/api/signup.go b/api/signup.go index df43c71d6..77057ff1a 100644 --- a/api/signup.go +++ b/api/signup.go @@ -54,8 +54,10 @@ func (a *Api) updateSignupConfirmation(newStatus models.Status, res http.Respons } if found != nil { updatedStatus := string(newStatus) + " signup" + a.logger(ctx).Debugf("new status: %s", updatedStatus) found.UpdateStatus(newStatus) + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer(updatedStatus) res.WriteHeader(http.StatusOK) @@ -156,6 +158,7 @@ func (a *Api) resendSignUp(res http.ResponseWriter, req *http.Request, vars map[ return } + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer("signup confirmation recreated") @@ -289,6 +292,7 @@ func (a *Api) acceptSignUp(res http.ResponseWriter, req *http.Request, vars map[ } found.UpdateStatus(models.StatusCompleted) + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer("accept signup") } @@ -496,6 +500,7 @@ func (a *Api) upsertSignUp(res http.ResponseWriter, req *http.Request, vars map[ return nil } + // addOrUpdateConfirmation logs and writes a response on errors if a.addOrUpdateConfirmation(ctx, newSignUp, res) { a.logMetric("signup confirmation created", req) diff --git a/go.work.sum b/go.work.sum index ec0d5f9b8..10484464b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,3 +1,129 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.9/go.mod h1:rpxevX/0Lqvlbc88b7Sc1SPNdyK1riNBTUU6JXhYNpM= +cloud.google.com/go/accessapproval v1.7.3/go.mod h1:4l8+pwIxGTNqSf4T3ds8nLO94NQf0W/KnMNuQ9PbnP8= +cloud.google.com/go/accesscontextmanager v1.8.3/go.mod h1:4i/JkF2JiFbhLnnpnfoTX5vRXfhf9ukhU1ANOTALTOQ= +cloud.google.com/go/aiplatform v1.51.2/go.mod h1:hCqVYB3mY45w99TmetEoe8eCQEwZEp9WHxeZdcv9phw= +cloud.google.com/go/analytics v0.21.5/go.mod h1:BQtOBHWTlJ96axpPPnw5CvGJ6i3Ve/qX2fTxR8qWyr8= +cloud.google.com/go/apigateway v1.6.3/go.mod h1:k68PXWpEs6BVDTtnLQAyG606Q3mz8pshItwPXjgv44Y= +cloud.google.com/go/apigeeconnect v1.6.3/go.mod h1:peG0HFQ0si2bN15M6QSjEW/W7Gy3NYkWGz7pFz13cbo= +cloud.google.com/go/apigeeregistry v0.8.1/go.mod h1:MW4ig1N4JZQsXmBSwH4rwpgDonocz7FPBSw6XPGHmYw= +cloud.google.com/go/appengine v1.8.3/go.mod h1:2oUPZ1LVZ5EXi+AF1ihNAF+S8JrzQ3till5m9VQkrsk= +cloud.google.com/go/area120 v0.8.3/go.mod h1:5zj6pMzVTH+SVHljdSKC35sriR/CVvQZzG/Icdyriw0= +cloud.google.com/go/artifactregistry v1.14.4/go.mod h1:SJJcZTMv6ce0LDMUnihCN7WSrI+kBSFV0KIKo8S8aYU= +cloud.google.com/go/asset v1.15.2/go.mod h1:B6H5tclkXvXz7PD22qCA2TDxSVQfasa3iDlM89O2NXs= +cloud.google.com/go/assuredworkloads v1.11.3/go.mod h1:vEjfTKYyRUaIeA0bsGJceFV2JKpVRgyG2op3jfa59Zs= +cloud.google.com/go/automl v1.13.3/go.mod h1:Y8KwvyAZFOsMAPqUCfNu1AyclbC6ivCUF/MTwORymyY= +cloud.google.com/go/baremetalsolution v1.2.2/go.mod h1:O5V6Uu1vzVelYahKfwEWRMaS3AbCkeYHy3145s1FkhM= +cloud.google.com/go/batch v1.6.1/go.mod h1:urdpD13zPe6YOK+6iZs/8/x2VBRofvblLpx0t57vM98= +cloud.google.com/go/beyondcorp v1.0.2/go.mod h1:m8cpG7caD+5su+1eZr+TSvF6r21NdLJk4f9u4SP2Ntc= +cloud.google.com/go/bigquery v1.56.0/go.mod h1:KDcsploXTEY7XT3fDQzMUZlpQLHzE4itubHrnmhUrZA= +cloud.google.com/go/billing v1.17.3/go.mod h1:z83AkoZ7mZwBGT3yTnt6rSGI1OOsHSIi6a5M3mJ8NaU= +cloud.google.com/go/binaryauthorization v1.7.2/go.mod h1:kFK5fQtxEp97m92ziy+hbu+uKocka1qRRL8MVJIgjv0= +cloud.google.com/go/certificatemanager v1.7.3/go.mod h1:T/sZYuC30PTag0TLo28VedIRIj1KPGcOQzjWAptHa00= +cloud.google.com/go/channel v1.17.2/go.mod h1:aT2LhnftnyfQceFql5I/mP8mIbiiJS4lWqgXA815zMk= +cloud.google.com/go/cloudbuild v1.14.2/go.mod h1:Bn6RO0mBYk8Vlrt+8NLrru7WXlQ9/RDWz2uo5KG1/sg= +cloud.google.com/go/clouddms v1.7.2/go.mod h1:Rk32TmWmHo64XqDvW7jgkFQet1tUKNVzs7oajtJT3jU= +cloud.google.com/go/cloudtasks v1.12.3/go.mod h1:GPVXhIOSGEaR+3xT4Fp72ScI+HjHffSS4B8+BaBB5Ys= +cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.11.2/go.mod h1:A9PIR5ov5cRcd28KlDbmmXE8Aay+Gccer2h4wzkYFso= +cloud.google.com/go/container v1.26.2/go.mod h1:YlO84xCt5xupVbLaMY4s3XNE79MUJ+49VmkInr6HvF4= +cloud.google.com/go/containeranalysis v0.11.2/go.mod h1:xibioGBC1MD2j4reTyV1xY1/MvKaz+fyM9ENWhmIeP8= +cloud.google.com/go/datacatalog v1.18.2/go.mod h1:SPVgWW2WEMuWHA+fHodYjmxPiMqcOiWfhc9OD5msigk= +cloud.google.com/go/dataflow v0.9.3/go.mod h1:HI4kMVjcHGTs3jTHW/kv3501YW+eloiJSLxkJa/vqFE= +cloud.google.com/go/dataform v0.8.3/go.mod h1:8nI/tvv5Fso0drO3pEjtowz58lodx8MVkdV2q0aPlqg= +cloud.google.com/go/datafusion v1.7.3/go.mod h1:eoLt1uFXKGBq48jy9LZ+Is8EAVLnmn50lNncLzwYokE= +cloud.google.com/go/datalabeling v0.8.3/go.mod h1:tvPhpGyS/V7lqjmb3V0TaDdGvhzgR1JoW7G2bpi2UTI= +cloud.google.com/go/dataplex v1.10.2/go.mod h1:xdC8URdTrCrZMW6keY779ZT1cTOfV8KEPNsw+LTRT1Y= +cloud.google.com/go/dataproc/v2 v2.2.2/go.mod h1:aocQywVmQVF4i8CL740rNI/ZRpsaaC1Wh2++BJ7HEJ4= +cloud.google.com/go/dataqna v0.8.3/go.mod h1:wXNBW2uvc9e7Gl5k8adyAMnLush1KVV6lZUhB+rqNu4= +cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/datastream v1.10.2/go.mod h1:W42TFgKAs/om6x/CdXX5E4oiAsKlH+e8MTGy81zdYt0= +cloud.google.com/go/deploy v1.14.1/go.mod h1:N8S0b+aIHSEeSr5ORVoC0+/mOPUysVt8ae4QkZYolAw= +cloud.google.com/go/dialogflow v1.44.2/go.mod h1:QzFYndeJhpVPElnFkUXxdlptx0wPnBWLCBT9BvtC3/c= +cloud.google.com/go/dlp v1.10.3/go.mod h1:iUaTc/ln8I+QT6Ai5vmuwfw8fqTk2kaz0FvCwhLCom0= +cloud.google.com/go/documentai v1.23.4/go.mod h1:4MYAaEMnADPN1LPN5xboDR5QVB6AgsaxgFdJhitlE2Y= +cloud.google.com/go/domains v0.9.3/go.mod h1:29k66YNDLDY9LCFKpGFeh6Nj9r62ZKm5EsUJxAl84KU= +cloud.google.com/go/edgecontainer v1.1.3/go.mod h1:Ll2DtIABzEfaxaVSbwj3QHFaOOovlDFiWVDu349jSsA= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.6.4/go.mod h1:iju5Vy3d9tJUg0PYMd1nHhjV7xoCXaOAVabrwLaPBEM= +cloud.google.com/go/eventarc v1.13.2/go.mod h1:X9A80ShVu19fb4e5sc/OLV7mpFUKZMwfJFeeWhcIObM= +cloud.google.com/go/filestore v1.7.3/go.mod h1:Qp8WaEERR3cSkxToxFPHh/b8AACkSut+4qlCjAmKTV0= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/functions v1.15.3/go.mod h1:r/AMHwBheapkkySEhiZYLDBwVJCdlRwsm4ieJu35/Ug= +cloud.google.com/go/gkebackup v1.3.3/go.mod h1:eMk7/wVV5P22KBakhQnJxWSVftL1p4VBFLpv0kIft7I= +cloud.google.com/go/gkeconnect v0.8.3/go.mod h1:i9GDTrfzBSUZGCe98qSu1B8YB8qfapT57PenIb820Jo= +cloud.google.com/go/gkehub v0.14.3/go.mod h1:jAl6WafkHHW18qgq7kqcrXYzN08hXeK/Va3utN8VKg8= +cloud.google.com/go/gkemulticloud v1.0.2/go.mod h1:+ee5VXxKb3H1l4LZAcgWB/rvI16VTNTrInWxDjAGsGo= +cloud.google.com/go/gsuiteaddons v1.6.3/go.mod h1:sCFJkZoMrLZT3JTb8uJqgKPNshH2tfXeCwTFRebTq48= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.4/go.mod h1:l/rg8l1AaA+VFMho/HYx2Vv6xinPSLMF8qfhRPIZ0L8= +cloud.google.com/go/iap v1.9.2/go.mod h1:GwDTOs047PPSnwRD0Us5FKf4WDRcVvHg1q9WVkKBhdI= +cloud.google.com/go/ids v1.4.3/go.mod h1:9CXPqI3GedjmkjbMWCUhMZ2P2N7TUMzAkVXYEH2orYU= +cloud.google.com/go/iot v1.7.3/go.mod h1:t8itFchkol4VgNbHnIq9lXoOOtHNR3uAACQMYbN9N4I= +cloud.google.com/go/kms v1.15.4/go.mod h1:L3Sdj6QTHK8dfwK5D1JLsAyELsNMnd3tAIwGS4ltKpc= +cloud.google.com/go/language v1.12.1/go.mod h1:zQhalE2QlQIxbKIZt54IASBzmZpN/aDASea5zl1l+J4= +cloud.google.com/go/lifesciences v0.9.3/go.mod h1:gNGBOJV80IWZdkd+xz4GQj4mbqaz737SCLHn2aRhQKM= +cloud.google.com/go/logging v1.8.1/go.mod h1:TJjR+SimHwuC8MZ9cjByQulAMgni+RkXeI3wwctHJEI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/longrunning v0.5.3/go.mod h1:y/0ga59EYu58J6SHmmQOvekvND2qODbu8ywBBW7EK7Y= +cloud.google.com/go/managedidentities v1.6.3/go.mod h1:tewiat9WLyFN0Fi7q1fDD5+0N4VUoL0SCX0OTCthZq4= +cloud.google.com/go/maps v1.5.1/go.mod h1:NPMZw1LJwQZYCfz4y+EIw+SI+24A4bpdFJqdKVr0lt4= +cloud.google.com/go/mediatranslation v0.8.3/go.mod h1:F9OnXTy336rteOEywtY7FOqCk+J43o2RF638hkOQl4Y= +cloud.google.com/go/memcache v1.10.3/go.mod h1:6z89A41MT2DVAW0P4iIRdu5cmRTsbsFn4cyiIx8gbwo= +cloud.google.com/go/metastore v1.13.2/go.mod h1:KS59dD+unBji/kFebVp8XU/quNSyo8b6N6tPGspKszA= +cloud.google.com/go/monitoring v1.16.2/go.mod h1:B44KGwi4ZCF8Rk/5n+FWeispDXoKSk9oss2QNlXJBgc= +cloud.google.com/go/networkconnectivity v1.14.2/go.mod h1:5UFlwIisZylSkGG1AdwK/WZUaoz12PKu6wODwIbFzJo= +cloud.google.com/go/networkmanagement v1.9.2/go.mod h1:iDGvGzAoYRghhp4j2Cji7sF899GnfGQcQRQwgVOWnDw= +cloud.google.com/go/networksecurity v0.9.3/go.mod h1:l+C0ynM6P+KV9YjOnx+kk5IZqMSLccdBqW6GUoF4p/0= +cloud.google.com/go/notebooks v1.11.1/go.mod h1:V2Zkv8wX9kDCGRJqYoI+bQAaoVeE5kSiz4yYHd2yJwQ= +cloud.google.com/go/optimization v1.6.1/go.mod h1:hH2RYPTTM9e9zOiTaYPTiGPcGdNZVnBSBxjIAJzUkqo= +cloud.google.com/go/orchestration v1.8.3/go.mod h1:xhgWAYqlbYjlz2ftbFghdyqENYW+JXuhBx9KsjMoGHs= +cloud.google.com/go/orgpolicy v1.11.3/go.mod h1:oKAtJ/gkMjum5icv2aujkP4CxROxPXsBbYGCDbPO8MM= +cloud.google.com/go/osconfig v1.12.3/go.mod h1:L/fPS8LL6bEYUi1au832WtMnPeQNT94Zo3FwwV1/xGM= +cloud.google.com/go/oslogin v1.12.1/go.mod h1:VfwTeFJGbnakxAY236eN8fsnglLiVXndlbcNomY4iZU= +cloud.google.com/go/phishingprotection v0.8.3/go.mod h1:3B01yO7T2Ra/TMojifn8EoGd4G9jts/6cIO0DgDY9J8= +cloud.google.com/go/policytroubleshooter v1.10.1/go.mod h1:5C0rhT3TDZVxAu8813bwmTvd57Phbl8mr9F4ipOsxEs= +cloud.google.com/go/privatecatalog v0.9.3/go.mod h1:K5pn2GrVmOPjXz3T26mzwXLcKivfIJ9R5N79AFCF9UE= +cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +cloud.google.com/go/recaptchaenterprise/v2 v2.8.2/go.mod h1:kpaDBOpkwD4G0GVMzG1W6Doy1tFFC97XAV3xy+Rd/pw= +cloud.google.com/go/recommendationengine v0.8.3/go.mod h1:m3b0RZV02BnODE9FeSvGv1qibFo8g0OnmB/RMwYy4V8= +cloud.google.com/go/recommender v1.11.2/go.mod h1:AeoJuzOvFR/emIcXdVFkspVXVTYpliRCmKNYDnyBv6Y= +cloud.google.com/go/redis v1.13.3/go.mod h1:vbUpCKUAZSYzFcWKmICnYgRAhTFg9r+djWqFxDYXi4U= +cloud.google.com/go/resourcemanager v1.9.3/go.mod h1:IqrY+g0ZgLsihcfcmqSe+RKp1hzjXwG904B92AwBz6U= +cloud.google.com/go/resourcesettings v1.6.3/go.mod h1:pno5D+7oDYkMWZ5BpPsb4SO0ewg3IXcmmrUZaMJrFic= +cloud.google.com/go/retail v1.14.3/go.mod h1:Omz2akDHeSlfCq8ArPKiBxlnRpKEBjUH386JYFLUvXo= +cloud.google.com/go/run v1.3.2/go.mod h1:SIhmqArbjdU/D9M6JoHaAqnAMKLFtXaVdNeq04NjnVE= +cloud.google.com/go/scheduler v1.10.3/go.mod h1:8ANskEM33+sIbpJ+R4xRfw/jzOG+ZFE8WVLy7/yGvbc= +cloud.google.com/go/secretmanager v1.11.3/go.mod h1:0bA2o6FabmShrEy328i67aV+65XoUFFSmVeLBn/51jI= +cloud.google.com/go/security v1.15.3/go.mod h1:gQ/7Q2JYUZZgOzqKtw9McShH+MjNvtDpL40J1cT+vBs= +cloud.google.com/go/securitycenter v1.24.1/go.mod h1:3h9IdjjHhVMXdQnmqzVnM7b0wMn/1O/U20eWVpMpZjI= +cloud.google.com/go/servicedirectory v1.11.2/go.mod h1:KD9hCLhncWRV5jJphwIpugKwM5bn1x0GyVVD4NO8mGg= +cloud.google.com/go/shell v1.7.3/go.mod h1:cTTEz/JdaBsQAeTQ3B6HHldZudFoYBOqjteev07FbIc= +cloud.google.com/go/spanner v1.51.0/go.mod h1:c5KNo5LQ1X5tJwma9rSQZsXNBDNvj4/n8BVc3LNahq0= +cloud.google.com/go/speech v1.19.2/go.mod h1:2OYFfj+Ch5LWjsaSINuCZsre/789zlcCI3SY4oAi2oI= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storagetransfer v1.10.2/go.mod h1:meIhYQup5rg9juQJdyppnA/WLQCOguxtk1pr3/vBWzA= +cloud.google.com/go/talent v1.6.4/go.mod h1:QsWvi5eKeh6gG2DlBkpMaFYZYrYUnIpo34f6/V5QykY= +cloud.google.com/go/texttospeech v1.7.3/go.mod h1:Av/zpkcgWfXlDLRYob17lqMstGZ3GqlvJXqKMp2u8so= +cloud.google.com/go/tpu v1.6.3/go.mod h1:lxiueqfVMlSToZY1151IaZqp89ELPSrk+3HIQ5HRkbY= +cloud.google.com/go/trace v1.10.3/go.mod h1:Ke1bgfc73RV3wUFml+uQp7EsDw4dGaETLxB7Iq/r4CY= +cloud.google.com/go/translate v1.9.2/go.mod h1:E3Tc6rUTsQkVrXW6avbUhKJSr7ZE3j7zNmqzXKHqRrY= +cloud.google.com/go/video v1.20.2/go.mod h1:lrixr5JeKNThsgfM9gqtwb6Okuqzfo4VrY2xynaViTA= +cloud.google.com/go/videointelligence v1.11.3/go.mod h1:tf0NUaGTjU1iS2KEkGWvO5hRHeCkFK3nPo0/cOZhZAo= +cloud.google.com/go/vision/v2 v2.7.4/go.mod h1:ynDKnsDN/0RtqkKxQZ2iatv3Dm9O+HfRb5djl7l4Vvw= +cloud.google.com/go/vmmigration v1.7.3/go.mod h1:ZCQC7cENwmSWlwyTrZcWivchn78YnFniEQYRWQ65tBo= +cloud.google.com/go/vmwareengine v1.0.2/go.mod h1:xMSNjIk8/itYrz1JA8nV3Ajg4L4n3N+ugP8JKzk3OaA= +cloud.google.com/go/vpcaccess v1.7.3/go.mod h1:YX4skyfW3NC8vI3Fk+EegJnlYFatA+dXK4o236EUCUc= +cloud.google.com/go/webrisk v1.9.3/go.mod h1:RUYXe9X/wBDXhVilss7EDLW9ZNa06aowPuinUOPCXH8= +cloud.google.com/go/websecurityscanner v1.6.3/go.mod h1:x9XANObUFR+83Cya3g/B9M/yoHVqzxPnFtgF8yYGAXw= +cloud.google.com/go/workflows v1.12.2/go.mod h1:+OmBIgNqYJPVggnMo9nqmizW0qEXHhmnAzK/CnBqsHc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -5,6 +131,7 @@ github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4s github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= +github.com/IBM/sarama v1.42.0/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= @@ -13,8 +140,11 @@ github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJs github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/Shopify/sarama v1.25.0/go.mod h1:y/CFFTO9eaMTNriwu/Q+W4eioLqiDMGkA1W+gmdfj8w= +github.com/Shopify/sarama v1.27.0/go.mod h1:aCdj6ymI8uyPEux1JJ9gcaDT6cinjGhNCAhs54taSUo= github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A= github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= @@ -23,6 +153,7 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/ant0ine/go-json-rest v3.3.2+incompatible h1:nBixrkLFiDNAW0hauKDLc8yJI6XfrQumWvytE1Hk14E= github.com/ant0ine/go-json-rest v3.3.2+incompatible/go.mod h1:q6aCt0GfU6LhpBsnZ/2U+mwe+0XB5WStbmwyoPfc+sk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -32,6 +163,8 @@ github.com/aws/aws-sdk-go v1.47.3 h1:e0H6NFXiniCpR8Lu3lTphVdRaeRCDLAeRyTHd1tJSd8 github.com/aws/aws-sdk-go v1.47.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -39,8 +172,12 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -48,25 +185,42 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.2.0/go.mod h1:XbBXL/a5TGNGs5N4UreDCIK7F71MNrXURJeVXury+XY= github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.14.0 h1:1MCVOxNZySIYOWMI1+6Z7YR0PK3AmDi/Fklk1KdFIv8= github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.14.0/go.mod h1:/B8nchIwQlr00jtE9bR0aoKaag7bO67xPM7r1DXCH4I= +github.com/cloudevents/sdk-go/v2 v2.0.0/go.mod h1:3CTrpB4+u7Iaj6fd7E2Xvm5IxMdRoaAhqaRVnOr2rCU= +github.com/cloudevents/sdk-go/v2 v2.2.0/go.mod h1:3CTrpB4+u7Iaj6fd7E2Xvm5IxMdRoaAhqaRVnOr2rCU= github.com/cloudevents/sdk-go/v2 v2.14.0 h1:Nrob4FwVgi5L4tV9lhjzZcjYqFVyJzsA56CwPaPfv6s= github.com/cloudevents/sdk-go/v2 v2.14.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUEeUfapHMUX1T5To= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/deepmap/oapi-codegen v1.9.0/go.mod h1:7t4DbSxmAffcTEgrWvsPYEE2aOARZ8ZKWp3hDuZkHNc= github.com/deepmap/oapi-codegen v1.10.0/go.mod h1:TvVmDQlUkFli9gFij/gtW1o+tFBr4qCHyv2zG+R0YZY= +github.com/deepmap/oapi-codegen v1.11.0/go.mod h1:k+ujhoQGxmQYBZBbxhOZNZf4j08qv5mC+OH+fFTnKxM= +github.com/deepmap/oapi-codegen v1.13.4/go.mod h1:/h5nFQbTAMz4S/WtBz8sBfamlGByYKDr21O2uoNgCYI= github.com/deepmap/oapi-codegen v1.16.2 h1:xGHx0dNqYfy9gE8a7AVgVM8Sd5oF9SEgePzP+UPAUXI= github.com/deepmap/oapi-codegen v1.16.2/go.mod h1:rdYoEA2GE+riuZ91DvpmBX9hJbQpuY9wchXpfQ3n+ho= github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= @@ -77,17 +231,34 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= +github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -105,15 +276,19 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/githubnemo/CompileDaemon v1.4.0/go.mod h1:/G125r3YBIp6rcXtCZfiEHwFzcl7GSsNSwylxSNrkMA= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -127,9 +302,12 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= @@ -137,14 +315,32 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofiber/fiber/v2 v2.49.1/go.mod h1:nPUeEBUeeYGgwbDm59Gp7vS8MDyScL6ezr/Np9A13WU= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -157,19 +353,37 @@ github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGS github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4= github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -177,18 +391,26 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gowebpki/jcs v1.0.1/go.mod h1:CID1cNZ+sHp1CCpAR8mPf6QRtagFBgPJE0FCUQ6+BrI= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= github.com/iris-contrib/httpexpect/v2 v2.15.2/go.mod h1:JLDgIqnFy5loDSUv1OA2j0mb6p/rDhiCqigP22Uq9xE= github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw= @@ -197,6 +419,8 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= @@ -233,6 +457,9 @@ github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA= github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= @@ -244,6 +471,7 @@ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8t github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -275,7 +503,9 @@ github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbq github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= github.com/lestrrat-go/jwx v1.2.23/go.mod h1:sAXjRwzSvCN6soO4RLoWWm1bVPpb8iOuv0IYfH8OWd8= +github.com/lestrrat-go/jwx v1.2.24/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= @@ -289,6 +519,7 @@ github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIG github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -298,6 +529,7 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -306,6 +538,7 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mediocregopher/radix/v3 v3.8.1/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= @@ -314,6 +547,7 @@ github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLT github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -332,14 +566,24 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= @@ -351,14 +595,27 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.2.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rinchsan/device-check-go v1.3.0/go.mod h1:xDdGHphsyiTYLfq36DlAn8M8ir2iyUS5nOMj62sF3hU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -390,6 +647,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -410,12 +668,18 @@ github.com/tdewolff/parse/v2 v2.7.4/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1 github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/tidepool-org/clinic/client v0.0.0-20230815132146-bd6c2982ff6d/go.mod h1:eduhUZw6oOhrtt2C57RGn4rYq9CoCX8ucwDV0PmxSF4= github.com/tidepool-org/clinic/client v0.0.0-20231026151906-ad2e71e79f6f h1:WUj7V54KaBeHWfCH4TZ4Jkw1J+pWXOd39E5cD5NhsbE= github.com/tidepool-org/clinic/client v0.0.0-20231026151906-ad2e71e79f6f/go.mod h1:eduhUZw6oOhrtt2C57RGn4rYq9CoCX8ucwDV0PmxSF4= +github.com/tidepool-org/devices/api v0.0.0-20220914225528-c7373eb1babc/go.mod h1:hiVnAb182K2eV2/ZqZGhi3v3qK7qJhBuDE4bR0HvIcE= +github.com/tidepool-org/go-common v0.10.1-0.20230508194719-72b56b95a79a/go.mod h1:hJ7gk9U6QhIJsVspA8EHN8YuKZuCd/HCP25II+D63w0= github.com/tidepool-org/go-common v0.11.0 h1:S5lGQlmYIVyfw58R7rDHmUaBwANb4Xf/MLjB2ZIFqDg= github.com/tidepool-org/go-common v0.11.0/go.mod h1:5U4rnYWGfg4gW/fT9EAZZnSIRsYH0ijIlkDnddL+OE4= +github.com/tidepool-org/hydrophone/client v0.0.0-20230915144349-ccec1a4d1782/go.mod h1:OTyfLgQuKg7xiU5PLMvxzwwOaUdZsbeIvVxTKrT0n+g= +github.com/tidepool-org/platform v1.33.1-0.20231013005639-3b2d96d57243/go.mod h1:MmUL8WfxdJtmRPHxh4byfEFrbdOvYIB9BriUaosQkLk= github.com/tidepool-org/platform v1.33.1-0.20231115170155-f8e6c2c3a407 h1:AQemKsMSHPM/SwnBO4rwiFOIe+sHohKuJGj5wDiSp6E= github.com/tidepool-org/platform v1.33.1-0.20231115170155-f8e6c2c3a407/go.mod h1:QXmXXVMbB1EInbccYxF45PT04xjZDhNqbHnWs3RPBhs= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -429,6 +693,7 @@ github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxW github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= @@ -445,12 +710,16 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw= github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -471,25 +740,55 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.10.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.13.1/go.mod h1:bREWhavnedxpJeTq9pQT53BbvwhUv7TcpsOqcH4a+3w= go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -498,18 +797,28 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -<<<<<<< HEAD +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -519,55 +828,83 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -||||||| parent of eb1e981 (move to zap logging) -======= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= ->>>>>>> eb1e981 (move to zap logging) golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -586,6 +923,7 @@ golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -595,6 +933,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= @@ -614,6 +953,7 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -622,12 +962,22 @@ golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191114200427-caa0b0f7d508/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -640,11 +990,61 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= +google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= +google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= +google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -654,19 +1054,33 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200601152816-913338de1bd2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +syreclabs.com/go/faker v1.2.3/go.mod h1:NAXInmkPsC2xuO5MKZFe80PUXX5LU8cFdJIHGs+nSBE= diff --git a/hydrophone.go b/hydrophone.go index 4355469f7..6a755da79 100644 --- a/hydrophone.go +++ b/hydrophone.go @@ -206,7 +206,7 @@ func startEventConsumer(p InvocationParams) { OnStart: func(ctx context.Context) error { go func() { if err := p.Consumer.Start(); err != nil { - p.Log.With(zap.Error(err)).Infof("starting cloud events consumer") + p.Log.With(zap.Error(err)).Error("starting cloud events consumer") p.Log.Infof("shutting down the service") if shutdownErr := p.Shutdowner.Shutdown(); shutdownErr != nil { p.Log.With(zap.Error(shutdownErr)).Error("failed to shutdown") From 9bf301e05bb0e068943de0797cd0fff60f96676f Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Wed, 29 Nov 2023 15:45:49 -0700 Subject: [PATCH 07/10] log the userID in a request's token, if found Requested in a previous code review. BACK-2500 --- api/hydrophoneApi.go | 9 +++++++++ api/signup.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 0f0ccb055..f545f59c4 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -440,6 +440,8 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma } // find and validate the token +// +// The token's userID field is added to the context's logger. func (a *Api) token(res http.ResponseWriter, req *http.Request) *shoreline.TokenData { ctx := req.Context() if token := req.Header.Get(TP_SESSION_TOKEN); token != "" { @@ -451,6 +453,13 @@ func (a *Api) token(res http.ResponseWriter, req *http.Request) *shoreline.Token return nil } //all good! + + ctxLog := a.logger(ctx).With(zap.String("token's userID", td.UserID)) + if td.IsServer { + ctxLog = a.logger(ctx).With(zap.String("token's userID", "")) + } + *req = *req.WithContext(context.WithValue(ctx, ctxLoggerKey{}, ctxLog)) + return td } a.sendError(ctx, res, http.StatusUnauthorized, STATUS_NO_TOKEN) diff --git a/api/signup.go b/api/signup.go index 77057ff1a..c641866f0 100644 --- a/api/signup.go +++ b/api/signup.go @@ -327,8 +327,8 @@ func (a *Api) dismissSignUp(res http.ResponseWriter, req *http.Request, vars map // status: 200 with a single result in an array // status: 404 func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { - ctx := req.Context() if token := a.token(res, req); token != nil { + ctx := req.Context() userId := vars["userid"] From 990951f51738c06adcad9fc63b6dd2737adee4a9 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 30 Nov 2023 09:33:54 -0700 Subject: [PATCH 08/10] changes requested in code review - fixes a logic error (re-factor gone wrong) - some logging tweaks to level or structured variables - re-worked sendClinicianConfirmation down to 2 return values - removed findResetConfirmation (low value, confusing results) - re-worked splitExtrasAndErrorsAndFields to return a struct Next commit will have a few more related tweaks. BACK-2500 --- api/clinic.go | 3 +-- api/clinicianInvites.go | 22 ++++++++++---------- api/forgot.go | 31 ++++++--------------------- api/hydrophoneApi.go | 46 ++++++++++++++++++++++++----------------- api/patientInvites.go | 1 - events/events.go | 21 +++++++++++++++---- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/api/clinic.go b/api/clinic.go index 678787434..96c50b49d 100644 --- a/api/clinic.go +++ b/api/clinic.go @@ -3,7 +3,6 @@ package api import ( "context" "encoding/json" - "fmt" "net/http" "go.uber.org/zap" @@ -89,7 +88,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ existingInvite, err := a.checkForDuplicateClinicInvite(ctx, clinicId, inviterID) if err != nil { a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, - fmt.Sprintf("clinic %s user already has or had an invite from %v", clinicId, inviterID)) + zap.String("inviterID", inviterID), "clinic already has or had an invite") return } if existingInvite { diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index 9c2ad3f94..c2e3ec930 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -75,9 +75,9 @@ func (a *Api) SendClinicianInvite(res http.ResponseWriter, req *http.Request, va return } - code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) - if code != 0 { - a.sendError(ctx, res, code, msg, optionalErr) + msg, err := a.sendClinicianConfirmation(req, confirmation) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, msg, err) return } @@ -147,9 +147,9 @@ func (a *Api) ResendClinicianInvite(res http.ResponseWriter, req *http.Request, confirmation.UserId = invitedUsr.UserID } - code, msg, optionalErr := a.sendClinicianConfirmation(req, confirmation) - if code != 0 { - a.sendError(ctx, res, code, msg, optionalErr) + msg, err := a.sendClinicianConfirmation(req, confirmation) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, msg, err) return } @@ -365,16 +365,16 @@ func (a *Api) CancelClinicianInvite(res http.ResponseWriter, req *http.Request, } } -func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) (code int, msg string, err error) { +func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models.Confirmation) (msg string, err error) { ctx := req.Context() if err := a.addProfile(confirmation); err != nil { a.logger(ctx).With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) - return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err + return STATUS_ERR_SAVING_CONFIRMATION, err } confirmation.Modified = time.Now() if err := a.Store.UpsertConfirmation(ctx, confirmation); err != nil { - return http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err + return STATUS_ERR_SAVING_CONFIRMATION, err } a.logMetric("clinician_invite_created", req) @@ -394,11 +394,11 @@ func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models. } if !a.createAndSendNotification(req, confirmation, emailContent) { - return http.StatusInternalServerError, STATUS_ERR_SENDING_EMAIL, nil + return STATUS_ERR_SENDING_EMAIL, nil } a.logMetric("clinician_invite_sent", req) - return 0, "", nil + return "", nil } func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http.Request, filter, conf *models.Confirmation, statusUpdate models.Status) { diff --git a/api/forgot.go b/api/forgot.go index d000d5196..95096dff0 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -1,7 +1,6 @@ package api import ( - "context" "encoding/json" "net/http" @@ -70,7 +69,6 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map resetCnf.Email = email //there is nothing more to do other than notify the user resetCnf.UpdateStatus(models.StatusCompleted) - a.logger(ctx).With(zap.String("email", email)).Info(STATUS_RESET_NO_ACCOUNT) } // addOrUpdateConfirmation logs and writes a response on errors @@ -92,23 +90,6 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map res.WriteHeader(http.StatusOK) } -// find the reset confirmation if it exists and hasn't expired -func (a *Api) findResetConfirmation(ctx context.Context, conf *models.Confirmation) (*models.Confirmation, bool, error) { - a.logger(ctx).With("conf", conf).Debug("finding reset confirmation") - found, err := a.Store.FindConfirmation(ctx, conf) - if err != nil { - return nil, false, err - } - if found == nil { - return nil, false, nil - } - if found.IsExpired() { - return nil, true, nil - } - - return found, false, nil -} - // Accept the password change // // This call will be invoked by the lost password screen with the key that was included in the URL of the lost password screen. @@ -135,19 +116,19 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma resetCnf := &models.Confirmation{Key: rb.Key, Email: rb.Email, Status: models.StatusPending, Type: models.TypePasswordReset} - conf, expired, err := a.findResetConfirmation(ctx, resetCnf) + conf, err := a.Store.FindConfirmation(ctx, resetCnf) if err != nil { a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } - if expired { - a.sendError(ctx, res, http.StatusNotFound, STATUS_RESET_EXPIRED) - return - } if conf == nil { a.sendError(ctx, res, http.StatusNotFound, STATUS_RESET_NOT_FOUND) return } + if conf.IsExpired() { + a.sendError(ctx, res, http.StatusNotFound, STATUS_RESET_EXPIRED) + return + } if resetCnf.Key == "" || resetCnf.Email != conf.Email { a.sendError(ctx, res, http.StatusBadRequest, STATUS_RESET_ERROR) @@ -164,7 +145,7 @@ func (a *Api) acceptPassword(res http.ResponseWriter, req *http.Request, vars ma } conf.UpdateStatus(models.StatusCompleted) // addOrUpdateConfirmation logs and writes a response on errors - if !a.addOrUpdateConfirmation(ctx, conf, res) { + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetricAsServer("password reset") a.sendOK(ctx, res, STATUS_RESET_ACCEPTED) return diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index f545f59c4..033f8003b 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -241,10 +241,10 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { // POST /confirm/resend/signup/:useremail // POST /confirm/resend/invite/:inviteId c.Handle("/resend/signup/{useremail}", vars(a.resendSignUp)).Methods("POST") - c.Handle("/resend/invite/{inviteId}", uid(vars(a.ResendInvite))).Methods("PATCH") + c.Handle("/resend/invite/{inviteId}", vars(a.ResendInvite)).Methods("PATCH") rtr.Handle("/resend/signup/{useremail}", vars(a.resendSignUp)).Methods("POST") - rtr.Handle("/resend/invite/{inviteId}", uid(vars(a.ResendInvite))).Methods("PATCH") + rtr.Handle("/resend/invite/{inviteId}", vars(a.ResendInvite)).Methods("PATCH") // PUT /confirm/accept/signup/:confirmationID // PUT /confirm/accept/forgot/ @@ -579,17 +579,17 @@ func (a *Api) sendErrorWithCode(ctx context.Context, res http.ResponseWriter, st } func (a *Api) sendErrorLog(ctx context.Context, code int, reason string, extras ...interface{}) { - nonErrs, errs, fields := splitExtrasAndErrorsAndFields(extras) + details := splitExtrasAndErrorsAndFields(extras) log := a.logger(ctx).WithOptions(zap.AddCallerSkip(2)). - Desugar().With(fields...).Sugar(). + Desugar().With(details.Fields...).Sugar(). With(zap.Int("code", code)). - With(zap.Array("extras", zapArrayAny(nonErrs))) - if len(errs) == 1 { - log = log.With(zap.Error(errs[0])) - } else if len(errs) > 1 { - log = log.With(zap.Errors("errors", errs)) + With(zap.Array("extras", zapArrayAny(details.NonErrors))) + if len(details.Errors) == 1 { + log = log.With(zap.Error(details.Errors[0])) + } else if len(details.Errors) > 1 { + log = log.With(zap.Errors("errors", details.Errors)) } - if code < http.StatusInternalServerError || len(errs) == 0 { + if code < http.StatusInternalServerError || len(details.Errors) == 0 { // if there are no errors, use info to skip the stack trace, as it's // probably not useful log.Info(reason) @@ -603,26 +603,34 @@ func (a *Api) sendOK(ctx context.Context, res http.ResponseWriter, reason string a.sendModelAsResWithStatus(ctx, res, status.NewStatus(http.StatusOK, reason), http.StatusOK) } -func splitExtrasAndErrorsAndFields(extras []interface{}) ([]interface{}, []error, []zapcore.Field) { - errs := []error{} - nonErrs := []interface{}{} - fields := []zap.Field{} +type extrasDetails struct { + Errors []error + NonErrors []interface{} + Fields []zap.Field +} + +func splitExtrasAndErrorsAndFields(extras []interface{}) extrasDetails { + details := extrasDetails{ + Errors: []error{}, + NonErrors: []interface{}{}, + Fields: []zap.Field{}, + } for _, extra := range extras { if err, ok := extra.(error); ok { if err != nil { - errs = append(errs, err) + details.Errors = append(details.Errors, err) } } else if field, ok := extra.(zap.Field); ok { - fields = append(fields, field) + details.Fields = append(details.Fields, field) } else if extraErrs, ok := extra.([]error); ok { if len(extraErrs) > 0 { - errs = append(errs, extraErrs...) + details.Errors = append(details.Errors, extraErrs...) } } else { - nonErrs = append(nonErrs, extra) + details.NonErrors = append(details.NonErrors, extra) } } - return nonErrs, errs, fields + return details } func (a *Api) tokenUserHasRequestedPermissions(tokenData *shoreline.TokenData, groupId string, requestedPermissions commonClients.Permissions) (commonClients.Permissions, error) { diff --git a/api/patientInvites.go b/api/patientInvites.go index 5287df825..7132ddca0 100644 --- a/api/patientInvites.go +++ b/api/patientInvites.go @@ -100,7 +100,6 @@ func (a *Api) AcceptPatientInvite(res http.ResponseWriter, req *http.Request, va conf.UpdateStatus(models.StatusCompleted) // addOrUpdateConfirmation logs and writes a response on errors if !a.addOrUpdateConfirmation(ctx, conf, res) { - a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return } diff --git a/events/events.go b/events/events.go index d02325f65..ea5e98097 100644 --- a/events/events.go +++ b/events/events.go @@ -4,6 +4,8 @@ import ( "context" "time" + "go.uber.org/zap" + "github.com/tidepool-org/go-common/events" "github.com/tidepool-org/hydrophone/clients" ) @@ -13,21 +15,32 @@ const deleteTimeout = 60 * time.Second type handler struct { events.NoopUserEventsHandler - store clients.StoreClient + store clients.StoreClient + logger *zap.SugaredLogger } var _ events.UserEventsHandler = &handler{} -func NewHandler(store clients.StoreClient) events.EventHandler { +func NewHandler(store clients.StoreClient, logger *zap.SugaredLogger) events.EventHandler { return events.NewUserEventsHandler(&handler{ - store: store, + store: store, + logger: logger, }) } func (h *handler) HandleDeleteUserEvent(payload events.DeleteUserEvent) error { + var err error ctx, cancel := context.WithTimeout(context.Background(), deleteTimeout) defer cancel() - if err := h.store.RemoveConfirmationsForUser(ctx, payload.UserID); err != nil { + defer func(err *error) { + log := h.logger.With(zap.String("userId", payload.UserID)) + if err != nil { + log.With(zap.Error(*err)).Error("deleting confirmations") + } else { + log.With().Info("successfully deleted confirmations") + } + }(&err) + if err = h.store.RemoveConfirmationsForUser(ctx, payload.UserID); err != nil { return err } return nil From 0b8b968a6a1f3a8f3472b9946455a05d48884c00 Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 30 Nov 2023 10:08:53 -0700 Subject: [PATCH 09/10] add clinicId to the logging context As requested in code review. The addUserIDToLogger method was made general-purpose. In addition, instead of using it only on those handlers that have a userId/clinicId parameter, it's used as a generic middleware on all requests. This small performance hit makes it so that any future endpoints that are added will automatically log userid and clinicid, plus the endpoint definition code is a bit cleaner, as it's not having to call uid helpers all over the place. BACK-2500 --- api/clinic.go | 2 +- api/hydrophoneApi.go | 107 ++++++++++++++++++++------------------ api/hydrophoneApi_test.go | 28 ++++++++-- 3 files changed, 81 insertions(+), 56 deletions(-) diff --git a/api/clinic.go b/api/clinic.go index 96c50b49d..e82232fdd 100644 --- a/api/clinic.go +++ b/api/clinic.go @@ -93,7 +93,7 @@ func (a *Api) InviteClinic(res http.ResponseWriter, req *http.Request, vars map[ } if existingInvite { a.sendError(ctx, res, http.StatusConflict, statusExistingInviteMessage, - zap.String("clinicId", clinicId), zap.String("inviterID", inviterID), err) + zap.String("inviterID", inviterID), err) return } diff --git a/api/hydrophoneApi.go b/api/hydrophoneApi.go index 033f8003b..945d9c4e7 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -159,27 +159,27 @@ func routerProvider(api *Api) *mux.Router { // RouterModule build a router var RouterModule = fx.Options(fx.Provide(routerProvider, apiConfigProvider)) -// addUserIDToLogger adds userID to the logging context. +// addPathVarToLogger adds a request's path variable to the logging context. // -// It uses the first matching userID it finds, additional userIDs (which -// shouldn't exist anyway) are ignored. -// -// This is effected via its type being that of a mux.MiddlewareFunc. -func (a *Api) addUserIDToLogger(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, orig *http.Request) { - vars := mux.Vars(orig) - next := orig - for key := range vars { - if !strings.EqualFold(key, "userid") { - continue +// It uses the first case-insensitive match of name it finds, additional occurrences of name are +// ignored. +func (a *Api) addPathVarToLogger(name string) mux.MiddlewareFunc { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, orig *http.Request) { + vars := mux.Vars(orig) + next := orig + for key := range vars { + if !strings.EqualFold(key, name) { + continue + } + ctxLog := a.logger(orig.Context()).With(zap.String(key, vars[key])) + ctxWithLog := context.WithValue(orig.Context(), ctxLoggerKey{}, ctxLog) + next = orig.WithContext(ctxWithLog) + break } - ctxLog := a.baseLogger.With(zap.String(key, vars[key])) - ctxWithLog := context.WithValue(orig.Context(), ctxLoggerKey{}, ctxLog) - next = orig.WithContext(ctxWithLog) - break - } - h.ServeHTTP(w, next) - }) + h.ServeHTTP(w, next) + }) + } } type ctxLoggerKey struct{} @@ -207,6 +207,9 @@ func (a *Api) ctxLoggerHandler(h http.Handler) http.Handler { func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { rtr.Use(mux.MiddlewareFunc(a.ctxLoggerHandler)) + rtr.Use(a.addPathVarToLogger("userid")) + rtr.Use(a.addPathVarToLogger("clinicid")) + c := rtr.PathPrefix("/confirm").Subrouter() c.HandleFunc("/status", a.IsReady).Methods("GET") @@ -218,8 +221,6 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { c.HandleFunc("/live", a.IsAlive).Methods("GET") rtr.HandleFunc("/live", a.IsAlive).Methods("GET") - // uid is a shortened name for this middleware - uid := a.addUserIDToLogger // vars is a shorthand for applying the varsHandler to an handler. type vars = varsHandler @@ -227,16 +228,16 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { // POST /confirm/send/forgot/:useremail // POST /confirm/send/invite/:userid csend := rtr.PathPrefix("/confirm/send").Subrouter() - csend.Handle("/signup/{userid}", uid(vars(a.sendSignUp))).Methods("POST") + csend.Handle("/signup/{userid}", vars(a.sendSignUp)).Methods("POST") csend.Handle("/forgot/{useremail}", vars(a.passwordReset)).Methods("POST") - csend.Handle("/invite/{userid}", uid(vars(a.SendInvite))).Methods("POST") - csend.Handle("/invite/{userId}/clinic", uid(vars(a.InviteClinic))).Methods("POST") + csend.Handle("/invite/{userid}", vars(a.SendInvite)).Methods("POST") + csend.Handle("/invite/{userId}/clinic", vars(a.InviteClinic)).Methods("POST") send := rtr.PathPrefix("/send").Subrouter() - send.Handle("/signup/{userid}", uid(vars(a.sendSignUp))).Methods("POST") + send.Handle("/signup/{userid}", vars(a.sendSignUp)).Methods("POST") send.Handle("/forgot/{useremail}", vars(a.passwordReset)).Methods("POST") - send.Handle("/invite/{userid}", uid(vars(a.SendInvite))).Methods("POST") - send.Handle("/invite/{userId}/clinic", uid(vars(a.InviteClinic))).Methods("POST") + send.Handle("/invite/{userid}", vars(a.SendInvite)).Methods("POST") + send.Handle("/invite/{userId}/clinic", vars(a.InviteClinic)).Methods("POST") // POST /confirm/resend/signup/:useremail // POST /confirm/resend/invite/:inviteId @@ -252,46 +253,46 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { caccept := rtr.PathPrefix("/confirm/accept").Subrouter() caccept.Handle("/signup/{confirmationid}", vars(a.acceptSignUp)).Methods("PUT") caccept.Handle("/forgot", vars(a.acceptPassword)).Methods("PUT") - caccept.Handle("/invite/{userid}/{invitedby}", uid(vars(a.AcceptInvite))).Methods("PUT") + caccept.Handle("/invite/{userid}/{invitedby}", vars(a.AcceptInvite)).Methods("PUT") accept := rtr.PathPrefix("/accept").Subrouter() accept.Handle("/signup/{confirmationid}", vars(a.acceptSignUp)).Methods("PUT") accept.Handle("/forgot", vars(a.acceptPassword)).Methods("PUT") - accept.Handle("/invite/{userid}/{invitedby}", uid(vars(a.AcceptInvite))).Methods("PUT") + accept.Handle("/invite/{userid}/{invitedby}", vars(a.AcceptInvite)).Methods("PUT") // GET /confirm/signup/:userid // GET /confirm/invite/:userid - c.Handle("/signup/{userid}", uid(vars(a.getSignUp))).Methods("GET") - c.Handle("/invite/{userid}", uid(vars(a.GetSentInvitations))).Methods("GET") + c.Handle("/signup/{userid}", vars(a.getSignUp)).Methods("GET") + c.Handle("/invite/{userid}", vars(a.GetSentInvitations)).Methods("GET") - rtr.Handle("/signup/{userid}", uid(vars(a.getSignUp))).Methods("GET") - rtr.Handle("/invite/{userid}", uid(vars(a.GetSentInvitations))).Methods("GET") + rtr.Handle("/signup/{userid}", vars(a.getSignUp)).Methods("GET") + rtr.Handle("/invite/{userid}", vars(a.GetSentInvitations)).Methods("GET") // GET /confirm/invitations/:userid - c.Handle("/invitations/{userid}", uid(vars(a.GetReceivedInvitations))).Methods("GET") + c.Handle("/invitations/{userid}", vars(a.GetReceivedInvitations)).Methods("GET") - rtr.Handle("/invitations/{userid}", uid(vars(a.GetReceivedInvitations))).Methods("GET") + rtr.Handle("/invitations/{userid}", vars(a.GetReceivedInvitations)).Methods("GET") // PUT /confirm/dismiss/invite/:userid/:invited_by // PUT /confirm/dismiss/signup/:userid cdismiss := rtr.PathPrefix("/confirm/dismiss").Subrouter() - cdismiss.Handle("/invite/{userid}/{invitedby}", uid(vars(a.DismissInvite))).Methods("PUT") - cdismiss.Handle("/signup/{userid}", uid(vars(a.dismissSignUp))).Methods("PUT") + cdismiss.Handle("/invite/{userid}/{invitedby}", vars(a.DismissInvite)).Methods("PUT") + cdismiss.Handle("/signup/{userid}", vars(a.dismissSignUp)).Methods("PUT") dismiss := rtr.PathPrefix("/dismiss").Subrouter() - dismiss.Handle("/invite/{userid}/{invitedby}", uid(vars(a.DismissInvite))).Methods("PUT") - dismiss.Handle("/signup/{userid}", uid(vars(a.dismissSignUp))).Methods("PUT") + dismiss.Handle("/invite/{userid}/{invitedby}", vars(a.DismissInvite)).Methods("PUT") + dismiss.Handle("/signup/{userid}", vars(a.dismissSignUp)).Methods("PUT") // POST /confirm/signup/:userid - c.Handle("/signup/{userid}", uid(vars(a.createSignUp))).Methods("POST") + c.Handle("/signup/{userid}", vars(a.createSignUp)).Methods("POST") // PUT /confirm/:userid/invited/:invited_address // PUT /confirm/signup/:userid - c.Handle("/{userid}/invited/{invited_address}", uid(vars(a.CancelInvite))).Methods("PUT") - c.Handle("/signup/{userid}", uid(vars(a.cancelSignUp))).Methods("PUT") + c.Handle("/{userid}/invited/{invited_address}", vars(a.CancelInvite)).Methods("PUT") + c.Handle("/signup/{userid}", vars(a.cancelSignUp)).Methods("PUT") - rtr.Handle("/{userid}/invited/{invited_address}", uid(vars(a.CancelInvite))).Methods("PUT") - rtr.Handle("/signup/{userid}", uid(vars(a.cancelSignUp))).Methods("PUT") + rtr.Handle("/{userid}/invited/{invited_address}", vars(a.CancelInvite)).Methods("PUT") + rtr.Handle("/signup/{userid}", vars(a.cancelSignUp)).Methods("PUT") // GET /v1/clinics/:clinicId/invites/patients // GET /v1/clinics/:clinicId/invites/patients/:inviteId @@ -303,13 +304,13 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.AcceptPatientInvite)).Methods("PUT") rtr.Handle("/v1/clinics/{clinicId}/invites/patients/{inviteId}", vars(a.CancelOrDismissPatientInvite)).Methods("DELETE") - c.Handle("/v1/clinicians/{userId}/invites", uid(vars(a.GetClinicianInvitations))).Methods("GET") - c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.AcceptClinicianInvite))).Methods("PUT") - c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.DismissClinicianInvite))).Methods("DELETE") + c.Handle("/v1/clinicians/{userId}/invites", vars(a.GetClinicianInvitations)).Methods("GET") + c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", vars(a.AcceptClinicianInvite)).Methods("PUT") + c.Handle("/v1/clinicians/{userId}/invites/{inviteId}", vars(a.DismissClinicianInvite)).Methods("DELETE") - rtr.Handle("/v1/clinicians/{userId}/invites", uid(vars(a.GetClinicianInvitations))).Methods("GET") - rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.AcceptClinicianInvite))).Methods("PUT") - rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", uid(vars(a.DismissClinicianInvite))).Methods("DELETE") + rtr.Handle("/v1/clinicians/{userId}/invites", vars(a.GetClinicianInvitations)).Methods("GET") + rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", vars(a.AcceptClinicianInvite)).Methods("PUT") + rtr.Handle("/v1/clinicians/{userId}/invites/{inviteId}", vars(a.DismissClinicianInvite)).Methods("DELETE") c.Handle("/v1/clinics/{clinicId}/invites/clinicians", vars(a.SendClinicianInvite)).Methods("POST") c.Handle("/v1/clinics/{clinicId}/invites/clinicians/{inviteId}", vars(a.ResendClinicianInvite)).Methods("PATCH") @@ -582,8 +583,10 @@ func (a *Api) sendErrorLog(ctx context.Context, code int, reason string, extras details := splitExtrasAndErrorsAndFields(extras) log := a.logger(ctx).WithOptions(zap.AddCallerSkip(2)). Desugar().With(details.Fields...).Sugar(). - With(zap.Int("code", code)). - With(zap.Array("extras", zapArrayAny(details.NonErrors))) + With(zap.Int("code", code)) + if len(details.NonErrors) > 0 { + log = log.With(zap.Array("extras", zapArrayAny(details.NonErrors))) + } if len(details.Errors) == 1 { log = log.With(zap.Error(details.Errors[0])) } else if len(details.Errors) > 1 { diff --git a/api/hydrophoneApi_test.go b/api/hydrophoneApi_test.go index 28c89ffb8..bdc98ecf2 100644 --- a/api/hydrophoneApi_test.go +++ b/api/hydrophoneApi_test.go @@ -329,7 +329,7 @@ func Test_TokenUserHasRequestedPermissions_FullMatch(t *testing.T) { } } -func TestAddUserIDToLogger(s *testing.T) { +func TestAddPathVarToLogger(s *testing.T) { s.Run("is request specific (and thread-safe)", func(t *testing.T) { // This test is designed to try to exacerbate thread-safety issues in // Api logging. Unfortunately, it can't 100% reliably produce an error @@ -348,7 +348,7 @@ func TestAddUserIDToLogger(s *testing.T) { handler := ht.handlerWithSync(len(userIDs)) logData := ht.captureLogs(func() { - ts := ht.Server(handler, ht.Api.addUserIDToLogger, vars) + ts := ht.Server(handler, ht.Api.addPathVarToLogger("userid"), vars) for i := 0; i < len(userIDs); i++ { go ts.Client().Get(ts.URL) } @@ -368,7 +368,7 @@ func TestAddUserIDToLogger(s *testing.T) { ht := newHydrophoneTest(t) logData := ht.captureLogs(func() { - ts := ht.Server(ht.handlerLog(), ht.Api.addUserIDToLogger, vars) + ts := ht.Server(ht.handlerLog(), ht.Api.addPathVarToLogger("userid"), vars) if _, err := ts.Client().Get(ts.URL); err != nil { t.Errorf("expected no error, got: %s", err) } @@ -379,6 +379,28 @@ func TestAddUserIDToLogger(s *testing.T) { t.Errorf("expected field %s, got: %s", expected, logData) } }) + + s.Run("includes both the clinicId and the userId", func(t *testing.T) { + vars := testutil.WithRotatingVars(map[string]string{"userId": "foo", "clinicId": "bar"}) + ht := newHydrophoneTest(t) + + logData := ht.captureLogs(func() { + ts := ht.Server(ht.handlerLog(), ht.Api.addPathVarToLogger("clinicid"), + ht.Api.addPathVarToLogger("userid"), vars) + if _, err := ts.Client().Get(ts.URL); err != nil { + t.Errorf("expected no error, got: %s", err) + } + }) + + expectedUserId := `"userId": "foo"` + if !strings.Contains(logData, expectedUserId) { + t.Errorf("expected field %s, got: %s", expectedUserId, logData) + } + expectedClinicId := `"clinicId": "bar"` + if !strings.Contains(logData, expectedClinicId) { + t.Errorf("expected field %s, got: %s", expectedClinicId, logData) + } + }) } type mockAlertsClient struct{} From f90713b93689dc5c85e88a7be734915e69a1f02b Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Fri, 1 Dec 2023 08:45:35 -0700 Subject: [PATCH 10/10] always return an error when there's a message As reported in code review, returning an error is the best indicator that something has gone wrong. BACK-2500 --- api/clinicianInvites.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/clinicianInvites.go b/api/clinicianInvites.go index c2e3ec930..30dd16f57 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -394,7 +394,9 @@ func (a *Api) sendClinicianConfirmation(req *http.Request, confirmation *models. } if !a.createAndSendNotification(req, confirmation, emailContent) { - return STATUS_ERR_SENDING_EMAIL, nil + // TODO: better to re-work createAndSendNotification to return a + // proper error. + return STATUS_ERR_SENDING_EMAIL, fmt.Errorf("sending email") } a.logMetric("clinician_invite_sent", req)