diff --git a/api/clinic.go b/api/clinic.go index eafae11d4..e82232fdd 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,19 +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_USR, 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.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(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -66,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 } @@ -79,26 +76,24 @@ 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err, + "checking if user is already a patient of clinic") 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(ctx, res, http.StatusConflict, statusExistingPatientMessage, + "user is already a patient of clinic") 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, + zap.String("inviterID", inviterID), "clinic already has or had an invite") 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(ctx, res, http.StatusConflict, statusExistingInviteMessage, + zap.String("inviterID", inviterID), err) return } @@ -114,9 +109,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 @@ -128,17 +123,18 @@ 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) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, invite, res) { 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(ctx).With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) return } else if !suppressEmail { fullName := invite.Creator.Profile.FullName @@ -158,7 +154,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 59bc288bb..30dd16f57 100644 --- a/api/clinicianInvites.go +++ b/api/clinicianInvites.go @@ -11,7 +11,6 @@ import ( 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,28 +26,26 @@ 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) + // assertClinicAdmin will log and send a response return } 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.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(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 } @@ -57,7 +54,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 } @@ -68,7 +65,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 { @@ -78,9 +75,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()) + msg, err := a.sendClinicianConfirmation(req, confirmation) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, msg, err) return } @@ -99,19 +96,19 @@ 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) + // assertClinicAdmin will log and send a response return } 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 { @@ -126,16 +123,15 @@ 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(ctx, filter) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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 @@ -146,18 +142,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 } - statusErr := a.sendClinicianConfirmation(req, confirmation) - if statusErr != nil { - a.sendError(res, statusErr.Code, statusErr.Reason, statusErr.Error()) + msg, err := a.sendClinicianConfirmation(req, confirmation) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, msg, err) return } - a.sendModelAsResWithStatus(res, confirmation, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, confirmation, http.StatusOK) return } } @@ -170,14 +166,14 @@ 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) + // assertClinicAdmin will log and send a response return } // 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 { @@ -192,18 +188,17 @@ 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(ctx, filter) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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 } } @@ -214,33 +209,35 @@ 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.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(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } - - if invites := a.checkFoundConfirmations(res, found, err); invites != nil { - a.ensureIdSet(req.Context(), userId, invites) + if len(found) == 0 { + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { + a.ensureIdSet(ctx, 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "error populating restriction in invites for user") return } - a.logger.Infof("found and checked %v invites", len(invites)) a.logMetric("get_clinician_invitations", req) - a.sendModelAsResWithStatus(res, invites, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Infof("invites found and checked: %d", len(invites)) return } } @@ -253,12 +250,12 @@ 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.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(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") return } @@ -269,36 +266,33 @@ 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(ctx, accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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.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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "error populating restriction in invites for uiser") 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(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.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) + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, conf, res) { return } @@ -316,11 +310,11 @@ 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.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(ctx, res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, + "token belongs to a different user or user doesn't exist") return } @@ -330,9 +324,9 @@ 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) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { @@ -351,7 +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 { - a.logger.Warnw("token owner is not clinic admin", err) + // assertClinicAdmin will log and send a response return } @@ -361,9 +355,9 @@ 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) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } @@ -371,18 +365,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) (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)} + a.logger(ctx).With(zap.Error(err)).Error(STATUS_ERR_ADDING_PROFILE) + return 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 STATUS_ERR_SAVING_CONFIRMATION, err } a.logMetric("clinician_invite_created", req) @@ -402,13 +394,13 @@ 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), - } + // 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) - return nil + return "", nil } func (a *Api) cancelClinicianInviteWithStatus(res http.ResponseWriter, req *http.Request, filter, conf *models.Confirmation, statusUpdate models.Status) { @@ -416,17 +408,14 @@ 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if conf != nil { conf.UpdateStatus(statusUpdate) + // addOrUpdateConfirmation logs and writes a response on errors 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,10 +430,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.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + 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) } } @@ -455,10 +444,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.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_UNAUTHORIZED)}, http.StatusUnauthorized) + 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 @@ -467,7 +456,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(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 02e1ac192..95096dff0 100644 --- a/api/forgot.go +++ b/api/forgot.go @@ -1,13 +1,12 @@ 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" ) @@ -42,7 +41,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,20 +50,19 @@ 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 { - log.Print(STATUS_RESET_NO_ACCOUNT) - log.Printf("email used [%s]", email) + 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(res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } @@ -73,7 +71,8 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map resetCnf.UpdateStatus(models.StatusCompleted) } - if a.addOrUpdateConfirmation(req.Context(), resetCnf, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, resetCnf, res) { a.logMetricAsServer("reset confirmation created") emailContent := map[string]interface{}{ @@ -85,39 +84,12 @@ 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 res.WriteHeader(http.StatusOK) } -// 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) - found, err := a.findExistingConfirmation(ctx, conf, res) - if err != nil { - log.Printf("findResetConfirmation: error [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) - return nil - } - 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 - } - 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 found -} - // 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. @@ -133,46 +105,50 @@ func (a *Api) findResetConfirmation(conf *models.Confirmation, ctx context.Conte // 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 { - 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(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err, + "acceptPassword: error decoding reset details") return } 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, err := a.Store.FindConfirmation(ctx, resetCnf) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + 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) + return + } + + token := a.sl.TokenProvide() + + 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(ctx, res, http.StatusBadRequest, STATUS_RESET_ERROR, err, "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) + // 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/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 1050a6ee7..945d9c4e7 100644 --- a/api/hydrophoneApi.go +++ b/api/hydrophoneApi.go @@ -4,16 +4,16 @@ import ( "context" "encoding/json" "fmt" - "log" "net/http" "reflect" - "runtime" "strings" + "sync" "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" @@ -36,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"` @@ -58,27 +59,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( @@ -105,7 +129,7 @@ func NewApi( seagull: seagull, alerts: alerts, templates: templates, - logger: logger, + baseLogger: logger, } } @@ -135,7 +159,56 @@ func routerProvider(api *Api) *mux.Router { // RouterModule build a router var RouterModule = fx.Options(fx.Provide(routerProvider, apiConfigProvider)) +// addPathVarToLogger adds a request's path variable to the logging context. +// +// 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 + } + 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)) + rtr.Use(a.addPathVarToLogger("userid")) + rtr.Use(a.addPathVarToLogger("clinicid")) c := rtr.PathPrefix("/confirm").Subrouter() @@ -148,103 +221,106 @@ func (a *Api) SetHandlers(prefix string, rtr *mux.Router) { c.HandleFunc("/live", a.IsAlive).Methods("GET") rtr.HandleFunc("/live", a.IsAlive).Methods("GET") + // 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}", vars(a.sendSignUp)).Methods("POST") + csend.Handle("/forgot/{useremail}", vars(a.passwordReset)).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}", 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}", vars(a.sendSignUp)).Methods("POST") + send.Handle("/forgot/{useremail}", vars(a.passwordReset)).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 - 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}", 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}", 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}", 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}", 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}", vars(a.getSignUp)).Methods("GET") + c.Handle("/invite/{userid}", 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}", vars(a.getSignUp)).Methods("GET") + rtr.Handle("/invite/{userid}", vars(a.GetSentInvitations)).Methods("GET") // GET /confirm/invitations/:userid - c.Handle("/invitations/{userid}", varsHandler(a.GetReceivedInvitations)).Methods("GET") + c.Handle("/invitations/{userid}", vars(a.GetReceivedInvitations)).Methods("GET") - rtr.Handle("/invitations/{userid}", varsHandler(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}", varsHandler(a.DismissInvite)).Methods("PUT") - cdismiss.Handle("/signup/{userid}", varsHandler(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}", varsHandler(a.DismissInvite)).Methods("PUT") - dismiss.Handle("/signup/{userid}", varsHandler(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}", varsHandler(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}", varsHandler(a.CancelInvite)).Methods("PUT") - c.Handle("/signup/{userid}", varsHandler(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}", varsHandler(a.CancelInvite)).Methods("PUT") - rtr.Handle("/signup/{userid}", varsHandler(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 - 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", 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", 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") + 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) { @@ -253,10 +329,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 { - log.Printf("Error getting status [%v]", err) - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, err.Error())} - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + 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) @@ -272,32 +347,17 @@ 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION, err) return false } 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 { 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 } @@ -306,32 +366,19 @@ 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(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(ctx).With(zap.Error(err)).Warn("getting profile") } - return results } + return results } // 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 { @@ -341,7 +388,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(ctx).With(zap.Error(err)).Warn("permissions check failed; falling back to non-alerting notification") } else if has { templateName = models.TemplateNameCareteamInviteWithAlerting } @@ -350,7 +397,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(ctx).With(zap.String("type", string(conf.Type))). + Info("unknown confirmation type") return false } } @@ -360,13 +408,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(ctx).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(ctx).With(zap.Error(err)).Error("executing email template") return false } @@ -379,7 +428,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, @@ -392,21 +441,29 @@ 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 != "" { 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(ctx, res, http.StatusForbidden, STATUS_INVALID_TOKEN, + zap.String("token", 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 } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusUnauthorized, STATUS_NO_TOKEN)} - a.sendModelAsResWithStatus(res, statusErr, http.StatusUnauthorized) + a.sendError(ctx, res, http.StatusUnauthorized, STATUS_NO_TOKEN) return nil } @@ -426,9 +483,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 { - log.Printf("Error [%s] trying to get existing users details", err.Error()) + a.logger(ctx).With(zap.Error(err)).Error("getting existing user details") return nil } else { return usr @@ -444,9 +501,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(ctx).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(ctx).With(zap.Error(err)).Warn("upserting confirmation") + } } } } @@ -499,9 +558,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 { - log.Printf("Error [%s] trying to send model [%s]", err.Error(), 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") @@ -510,42 +569,71 @@ 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] +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(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(ctx context.Context, code int, reason string, extras ...interface{}) { + details := splitExtrasAndErrorsAndFields(extras) + log := a.logger(ctx).WithOptions(zap.AddCallerSkip(2)). + Desugar().With(details.Fields...).Sugar(). + 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 { + log = log.With(zap.Errors("errors", details.Errors)) + } + 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) } 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(ctx context.Context, res http.ResponseWriter, reason string) { + a.sendModelAsResWithStatus(ctx, 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.NewStatus(statusCode, reason), statusCode) +type extrasDetails struct { + Errors []error + NonErrors []interface{} + Fields []zap.Field } -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] - } else { - file = "???" - line = 0 +func splitExtrasAndErrorsAndFields(extras []interface{}) extrasDetails { + details := extrasDetails{ + Errors: []error{}, + NonErrors: []interface{}{}, + Fields: []zap.Field{}, } - - messages := make([]string, len(extras)) - for index, extra := range extras { - messages[index] = fmt.Sprintf("%v", extra) + for _, extra := range extras { + if err, ok := extra.(error); ok { + if err != nil { + details.Errors = append(details.Errors, err) + } + } else if field, ok := extra.(zap.Field); ok { + details.Fields = append(details.Fields, field) + } else if extraErrs, ok := extra.([]error); ok { + if len(extraErrs) > 0 { + details.Errors = append(details.Errors, extraErrs...) + } + } else { + details.NonErrors = append(details.NonErrors, extra) + } } - - 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) + return details } func (a *Api) tokenUserHasRequestedPermissions(tokenData *shoreline.TokenData, groupId string, requestedPermissions commonClients.Permissions) (commonClients.Permissions, error) { @@ -565,3 +653,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..bdc98ecf2 100644 --- a/api/hydrophoneApi_test.go +++ b/api/hydrophoneApi_test.go @@ -1,20 +1,22 @@ package api import ( + "bytes" "context" "errors" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" + "os" "reflect" + "strings" + "sync" "testing" "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 +25,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" ) @@ -72,17 +75,23 @@ var ( return NewResponsableMockGatekeeper() })) - BaseModule = fx.Options( - clients.MockNotifierModule, - MockShorelineModule, - MockMetricsModule, - MockSeagullModule, - MockAlertsModule, - MockTemplatesModule, - MockConfigModule, - 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, @@ -100,7 +109,6 @@ func TestGetStatus_StatusOk(t *testing.T) { BaseModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&api), ) @@ -122,7 +130,6 @@ func TestGetStatus_StatusInternalServerError(t *testing.T) { BaseModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&api), ) @@ -135,10 +142,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 +230,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 +259,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 +285,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 +311,6 @@ func Test_TokenUserHasRequestedPermissions_FullMatch(t *testing.T) { ResponableModule, MockClinicsModule, fx.Supply(t), - fx.Supply(zap.NewNop().Sugar()), fx.Populate(&responsableHydrophone), fx.Populate(&gk), ) @@ -323,6 +329,80 @@ func Test_TokenUserHasRequestedPermissions_FullMatch(t *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 + // 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.addPathVarToLogger("userid"), 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.addPathVarToLogger("userid"), 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) + } + }) + + 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{} func newMockAlertsClient() *mockAlertsClient { @@ -360,3 +440,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 4e9f01845..6473f4ef4 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,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() { - log.Println(statusExistingInviteMessage) - log.Println("last invite not yet expired") + a.logger(ctx).With(zap.String("email", inviteeEmail), + zap.String("extra", "last invite not yet expired")). + Debug(statusExistingInviteMessage) return true } } @@ -72,15 +72,14 @@ 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 { - log.Printf("error checking if user is in group [%v]", err) + a.logger(ctx).With(zap.Error(err)).Error("checking if user is in group") } else if perms != nil { - log.Println(statusExistingMemberMessage) return true, invitedUsr } return false, invitedUsr @@ -96,6 +95,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 == "" { @@ -104,24 +104,29 @@ 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) + 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 } - 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.logger.Errorw("error while finding pending invites", zap.Error(err)) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err, + "while finding pending invites") + return } - - if invites := a.checkFoundConfirmations(res, found, err); invites != nil { - a.ensureIdSet(req.Context(), inviteeID, invites) - a.logger.Infof("found and have checked [%d] invites ", len(invites)) + if len(found) == 0 { + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + 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.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Debugf("invites found and checked: %d", len(invites)) return } } @@ -135,7 +140,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 == "" { @@ -144,18 +149,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_USR, 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) - if invitations := a.checkFoundConfirmations(res, found, err); invitations != nil { + found, err := a.Store.FindConfirmations(ctx, &models.Confirmation{CreatorId: invitorID, Type: models.TypeCareteamInvite}, models.StatusPending, models.StatusDeclined) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) + return + } + if len(found) == 0 { + a.sendError(ctx, res, http.StatusNotFound, STATUS_NOT_FOUND) + return + } + 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 } } @@ -168,54 +181,44 @@ 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 == "" { - log.Printf("AcceptInvite inviteeID %s or invitorID %s not set", inviteeID, invitorID) res.WriteHeader(http.StatusBadRequest) + a.logger(ctx). + 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(ctx, 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(ctx, 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(ctx).Info("no confirmation key set") return } - conf, err := a.findExistingConfirmation(req.Context(), accept, res) + conf, err := a.Store.FindConfirmation(ctx, accept) if err != nil { - log.Printf("AcceptInvite error while finding confirmation [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) 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(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -227,61 +230,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(ctx, 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(ctx, 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(ctx, 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(ctx, 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(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 { - 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, - ) + 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) { - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusInternalServerError, STATUS_ERR_SAVING_CONFIRMATION)} - log.Println("AcceptInvite ", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError) + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, conf, res) { return } a.logMetric("acceptinvite", req) @@ -298,7 +277,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"] @@ -308,10 +287,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_USR, 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 } @@ -322,23 +301,22 @@ 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(ctx, invite) + if err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return - } else if conf != nil { + } + if conf != nil { //cancel the invite conf.UpdateStatus(models.StatusCanceled) - - if a.addOrUpdateConfirmation(req.Context(), conf, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("canceled invite", req) res.WriteHeader(http.StatusOK) return } } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - log.Printf("CancelInvite: [%s]", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -347,7 +325,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"] @@ -358,16 +336,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(ctx, 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(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -376,23 +351,21 @@ 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(ctx, dismiss) + if err != nil { + a.sendError(ctx, 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) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, conf, res) { a.logMetric("dismissinvite", req) res.WriteHeader(http.StatusOK) return } } - statusErr := &status.StatusError{Status: status.NewStatus(http.StatusNotFound, statusInviteNotFoundMessage)} - log.Printf("DismissInvite: [%s]", statusErr.Error()) - a.sendModelAsResWithStatus(res, statusErr, http.StatusNotFound) + a.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } } @@ -408,7 +381,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) @@ -421,18 +394,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_USR, 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 { - 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(ctx, res, http.StatusBadRequest, STATUS_ERR_DECODING_CONFIRMATION, err) return } @@ -441,15 +412,12 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st return } - 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) + 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 @@ -460,22 +428,22 @@ 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.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(ctx, 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(ctx).With(zapPermsField(perms)).Info("permissions set") } templateName := models.TemplateNameCareteamInvite @@ -485,8 +453,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(ctx, res, http.StatusInternalServerError, STATUS_ERR_CREATING_CONFIRMATION, err) return } @@ -495,14 +462,15 @@ func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[st invite.UserId = invitedUsr.UserID } - if !a.addOrUpdateConfirmation(req.Context(), invite, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, invite, res) { return } a.logMetric("invite created", req) if err := a.addProfile(invite); err != nil { - log.Println("SendInvite: ", err.Error()) - 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" @@ -530,7 +498,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 } @@ -542,7 +510,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 @@ -554,38 +522,36 @@ 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(ctx, find) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return } if invite == nil || invite.ClinicId != "" { if invite.ClinicId != "" { - a.logger.Warn("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.Warn("cannot resend confirmation, because it doesn't exist") + a.logger(ctx).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(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_USR, 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) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, invite, res) { a.logMetric("invite updated", req) if err := a.addProfile(invite); err != nil { - a.logger.Warn("Resend invite", zap.Error(err)) + a.logger(ctx).With(zap.Error(err)).Warn(STATUS_ERR_ADDING_PROFILE) } else { fullName := invite.Creator.Profile.FullName if invite.Creator.Profile.Patient.IsOtherPerson { @@ -608,8 +574,16 @@ 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 } } } + +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 458558b0c..7132ddca0 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" ) @@ -24,21 +23,26 @@ 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 { - 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(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 - } else if invites := a.checkFoundConfirmations(res, found, err); invites != nil { - a.logger.Infof("found and checked %d confirmations", len(invites)) + } + if invites := a.addProfileInfoToConfirmations(ctx, found); invites != nil { a.logMetric("get_patient_invites", req) - a.sendModelAsResWithStatus(res, invites, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, invites, http.StatusOK) + a.logger(ctx).Debugf("confirmations found and checked: %d", len(invites)) return } } @@ -56,8 +60,8 @@ 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 { - a.logger.Errorw("token owner is not a clinic member", zap.Error(err)) return } @@ -66,16 +70,13 @@ 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(ctx, accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, 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.sendError(ctx, res, http.StatusNotFound, statusInviteNotFoundMessage) return } @@ -85,38 +86,25 @@ 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(ctx, 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(ctx, 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) + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, conf, res) { return } a.logMetric("accept_patient_invite", req) - a.sendModelAsResWithStatus(res, patient, http.StatusOK) + a.sendModelAsResWithStatus(ctx, res, patient, http.StatusOK) return } } @@ -138,24 +126,21 @@ 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(ctx, accept) if err != nil { - a.logger.Errorw("error while finding confirmation", zap.Error(err)) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, 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.sendError(ctx, res, http.StatusForbidden, statusInviteNotFoundMessage) return } 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 { - a.logger.Errorw("token owner is not a clinic member", zap.Error(err)) return } } @@ -166,22 +151,14 @@ 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(ctx, 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) + // addOrUpdateConfirmation logs and writes a response on errors + if !a.addOrUpdateConfirmation(ctx, conf, res) { return } @@ -191,7 +168,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 } } @@ -231,7 +208,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(ctx).With(zap.Any("perms", patient.Permissions)).Info("permissions set") return patient, nil } diff --git a/api/signup.go b/api/signup.go index 7f92a19fe..c641866f0 100644 --- a/api/signup.go +++ b/api/signup.go @@ -4,59 +4,29 @@ 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.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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) 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(ctx, res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return nil } @@ -65,40 +35,36 @@ 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 { - 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(ctx, 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(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_CONF) return } - found, err := a.findExistingConfirmation(req.Context(), fromBody, res) + 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" - log.Printf("updateSignupConfirmation: %s", updatedStatus) + a.logger(ctx).Debugf("new status: %s", updatedStatus) found.UpdateStatus(newStatus) - if a.addOrUpdateConfirmation(req.Context(), found, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer(updatedStatus) res.WriteHeader(http.StatusOK) 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(ctx, res, http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND) return } } @@ -115,14 +81,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 != "" { @@ -131,8 +98,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(ctx, res, http.StatusInternalServerError, STATUS_ERR_ADDING_PROFILE, err) return } if newSignUp.Creator.Profile != nil && newSignUp.Creator.Profile.FullName != "" { @@ -141,11 +107,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(ctx, 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(ctx). + With(zap.String("email", newSignUp.Email)). + With(zap.String("key", newSignUp.Key)). + Debug("sending email confirmation") emailContent := map[string]interface{}{ "Key": newSignUp.Key, @@ -163,7 +132,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") } } } @@ -174,38 +142,40 @@ 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 { - log.Printf("resendSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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 { - log.Printf("resendSignUp: error resetting key [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return } - if a.addOrUpdateConfirmation(req.Context(), found, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, found, res) { 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(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_USR, "resendSignUp: error getting user profile: ", err.Error()) + a.sendError(ctx, 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(ctx). + With(zap.String("email", found.Email)). + With(zap.String("key", found.Key)). + Debug("resending email confirmation") emailContent := map[string]interface{}{ "Key": found.Key, @@ -218,9 +188,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 != "" { @@ -232,7 +202,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") } } @@ -254,19 +223,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 == "" { - log.Printf("acceptSignUp %s", STATUS_SIGNUP_NO_CONF) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_CONF), http.StatusBadRequest) + 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, "acceptSignUp: expired") + a.sendError(ctx, res, http.StatusNotFound, STATUS_SIGNUP_EXPIRED) return } @@ -274,43 +243,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(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, "acceptSignUp: error decoding acceptance: ", err.Error()) + 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, "acceptSignUp: 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, "acceptSignUp: 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, "acceptSignUp: 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, "acceptSignUp: 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_USR, "acceptSignUp: error getting the users profile: ", err.Error()) + 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, "acceptSignUp: 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 } @@ -318,12 +287,13 @@ 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(ctx, res, http.StatusInternalServerError, STATUS_ERR_UPDATING_USER, err) return } found.UpdateStatus(models.StatusCompleted) - if a.addOrUpdateConfirmation(req.Context(), found, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, found, res) { a.logMetricAsServer("accept signup") } @@ -341,14 +311,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 == "" { - log.Printf("dismissSignUp %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } - log.Print("dismissSignUp: dismissing for ", userId) + a.logger(ctx).Debug("dismissing invite") a.updateSignupConfirmation(models.StatusDeclined, res, req) } @@ -358,37 +328,36 @@ func (a *Api) dismissSignUp(res http.ResponseWriter, req *http.Request, vars map // status: 404 func (a *Api) getSignUp(res http.ResponseWriter, req *http.Request, vars map[string]string) { if token := a.token(res, req); token != nil { + ctx := req.Context() 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(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_USR, 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 { - log.Printf("getSignUp %s", STATUS_SIGNUP_NOT_FOUND) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusNotFound, STATUS_SIGNUP_NOT_FOUND), http.StatusNotFound) + a.sendError(ctx, 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.sendModelAsResWithStatus(ctx, res, signup, http.StatusOK) + a.logger(ctx).Debug("found a pending signup") return } } @@ -402,56 +371,56 @@ 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) } } // 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 // 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 == "" { - log.Printf("upsertSignUp %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + 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_USR, 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.sendModelAsResWithStatus(res, status.StatusError{Status: status.NewStatus(http.StatusBadRequest, "error decoding payload")}, http.StatusBadRequest) + a.sendError(ctx, 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(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 { - log.Printf("upsertSignUp: error finding old invite [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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 { - log.Printf("upsertSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + if err := a.Store.RemoveConfirmation(ctx, existing); err != nil { + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_DELETING_CONFIRMATION, err) return nil } } @@ -460,10 +429,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 { - log.Printf("upsertSignUp: error [%s]\n", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_CONFIRMATION, err) return nil } else if newSignUp == nil { @@ -485,8 +453,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(ctx, res, http.StatusInternalServerError, STATUS_ERR_FINDING_USER, err) return nil } @@ -504,7 +471,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 } @@ -513,15 +480,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 { - log.Printf("upsertSignUp: error deleting old [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + 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 { - log.Printf("upsertSignUp: error resetting key [%s]", err.Error()) - a.sendModelAsResWithStatus(res, err, http.StatusInternalServerError) + a.sendError(ctx, res, http.StatusInternalServerError, STATUS_ERR_RESETTING_KEY, err) return nil } @@ -531,12 +496,12 @@ 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(ctx, res, http.StatusForbidden, STATUS_EXISTING_SIGNUP) return nil } - if a.addOrUpdateConfirmation(req.Context(), newSignUp, res) { + // addOrUpdateConfirmation logs and writes a response on errors + if a.addOrUpdateConfirmation(ctx, newSignUp, res) { a.logMetric("signup confirmation created", req) return newSignUp @@ -549,15 +514,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 == "" { - log.Printf("cancelSignUp: %s", STATUS_SIGNUP_NO_ID) - a.sendModelAsResWithStatus(res, status.NewStatus(http.StatusBadRequest, STATUS_SIGNUP_NO_ID), http.StatusBadRequest) + a.sendError(ctx, res, http.StatusBadRequest, STATUS_SIGNUP_NO_ID) return } - log.Print("cancelSignUp: canceling for ", userId) a.updateSignupConfirmation(models.StatusCanceled, res, req) + a.logger(ctx).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..ea5e98097 100644 --- a/events/events.go +++ b/events/events.go @@ -2,9 +2,10 @@ package events import ( "context" - "log" "time" + "go.uber.org/zap" + "github.com/tidepool-org/go-common/events" "github.com/tidepool-org/hydrophone/clients" ) @@ -14,22 +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 { - log.Printf("Deleting confirmations for user %v", payload.UserID) + 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 diff --git a/go.work.sum b/go.work.sum index 89d03c861..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,17 +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= +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= @@ -518,51 +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= 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= @@ -581,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= @@ -590,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= @@ -609,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= @@ -617,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= @@ -635,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= @@ -649,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 7dc1b84d1..6a755da79 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)).Error("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/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 +}