Skip to content

Commit

Permalink
support care team alerting invitations
Browse files Browse the repository at this point in the history
Clients sending an invitation with alerting permissions will trigger a
different email notification, and the alerting information is passed along in
the Confirmation's context, where it can be picked up and later applied.

Part of BACK-2500
  • Loading branch information
ewollesen committed Sep 9, 2023
1 parent 4716fb5 commit dd4091d
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 101 deletions.
6 changes: 6 additions & 0 deletions api/hydrophoneApi.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,12 @@ func (a *Api) createAndSendNotification(req *http.Request, conf *models.Confirma
templateName = models.TemplateNamePasswordReset
case models.TypeCareteamInvite:
templateName = models.TemplateNameCareteamInvite
has, err := conf.HasPermission("alerting")
if err != nil {
log.Printf("error checking permissions, will fallback to non-alerting: %s", err)
} else if has {
templateName = models.TemplateNameCareteamInviteWithAlerting
}
case models.TypeSignUp:
templateName = models.TemplateNameSignup
case models.TypeNoAccount:
Expand Down
198 changes: 121 additions & 77 deletions api/invite.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,22 @@ import (

const (
//Status message we return from the service
statusExistingInviteMessage = "There is already an existing invite"
statusExistingMemberMessage = "The user is already an existing member"
statusExistingPatientMessage = "The user is already a patient of the clinic"
statusInviteNotFoundMessage = "No matching invite was found"
statusForbiddenMessage = "Forbidden to perform requested operation"
statusExistingInviteMessage = "There is already an existing invite"
statusExistingMemberMessage = "The user is already an existing member"
statusExistingPatientMessage = "The user is already a patient of the clinic"
statusInviteNotFoundMessage = "No matching invite was found"
statusForbiddenMessage = "Forbidden to perform requested operation"
statusInternalServerErrorMessage = "Internal Server Error"
)

type (
//Invite details for generating a new invite
inviteBody struct {
Email string `json:"email"`
Permissions commonClients.Permissions `json:"permissions"`
}
)
// Invite details for generating a new invite
type inviteBody struct {
Email string `json:"email"`
models.CareTeamContext
// UnmarshalJSON prevents inviteBody from inheriting it from
// CareTeamContext.
UnmarshalJSON struct{} `json:"-"`
}

// Checks do they have an existing invite or are they already a team member
// Or are they an existing user and already in the group?
Expand Down Expand Up @@ -380,94 +382,136 @@ func (a *Api) DismissInvite(res http.ResponseWriter, req *http.Request, vars map
// status: 409 statusExistingMemberMessage - user is already part of the team
// status: 400
func (a *Api) SendInvite(res http.ResponseWriter, req *http.Request, vars map[string]string) {
if token := a.token(res, req); token != nil {
token := a.token(res, req) // a.token writes a response on failure
if token == nil {
return
}

invitorID := vars["userid"]
invitorID := vars["userid"]
if invitorID == "" {
res.WriteHeader(http.StatusBadRequest)
return
}

if invitorID == "" {
res.WriteHeader(http.StatusBadRequest)
return
}
requiredPerms := commonClients.Permissions{
"root": commonClients.Allowed,
"custodian": commonClients.Allowed,
}
permissions, err := a.tokenUserHasRequestedPermissions(token, invitorID, requiredPerms)
if err != nil {
a.sendError(res, http.StatusInternalServerError, STATUS_ERR_FINDING_USR, err)
return
} else if permissions["root"] == nil && permissions["custodian"] == nil {
a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED)
return
}

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)
return
} else if permissions["root"] == nil && permissions["custodian"] == nil {
a.sendError(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)
return
}

defer req.Body.Close()
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)
return
}
if ib.Email == "" || ib.Permissions == nil {
res.WriteHeader(http.StatusBadRequest)
return
}

if ib.Email == "" || ib.Permissions == nil {
res.WriteHeader(http.StatusBadRequest)
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),
}

if a.checkForDuplicateInvite(req.Context(), ib.Email, invitorID) {
log.Printf("SendInvite: invited [%s] user already has or had an invite", ib.Email)
statusErr := &status.StatusError{
Status: status.NewStatus(http.StatusConflict, statusExistingInviteMessage),
}
a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict)
a.sendModelAsResWithStatus(res, statusErr, http.StatusConflict)
return
}
alreadyMember, invitedUsr := a.checkAccountAlreadySharedWithUser(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
// the Care Team Alerting features, users with existing relationships
// should be able to send a new invite that adds alerting
// permissions. As a result, this code now checks if the current
// invitation would add alerting permissions, and if so, allows it to
// continue.
perms, err := a.gatekeeper.UserInGroup(invitedUsr.UserID, invitorID)
if err != nil {
a.sendError(res, http.StatusInternalServerError, statusInternalServerErrorMessage)
return
} else if alreadyMember, invitedUsr := a.checkAccountAlreadySharedWithUser(invitorID, ib.Email); alreadyMember {
}
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)
return
} else {
//None exist so lets create the invite
invite, _ := models.NewConfirmationWithContext(models.TypeCareteamInvite, models.TemplateNameCareteamInvite, invitorID, ib.Permissions)
}
for key := range perms {
log.Printf("adding permission: %q %+v", key, perms[key])
ib.Permissions[key] = perms[key]
}
}

invite.Email = ib.Email
if invitedUsr != nil {
invite.UserId = invitedUsr.UserID
}
templateName := models.TemplateNameCareteamInvite
if ib.Permissions["alerting"] != nil {
templateName = models.TemplateNameCareteamInviteWithAlerting
}

if a.addOrUpdateConfirmation(req.Context(), invite, res) {
a.logMetric("invite created", req)
invite, err := models.NewConfirmationWithContext(models.TypeCareteamInvite, templateName, invitorID, ib.CareTeamContext)
if err != nil {
statusErr := &status.StatusError{Status: status.NewStatus(http.StatusConflict, statusInternalServerErrorMessage)}
a.sendModelAsResWithStatus(res, statusErr, http.StatusInternalServerError)
return
}

if err := a.addProfile(invite); err != nil {
log.Println("SendInvite: ", err.Error())
} else {
invite.Email = ib.Email
if invitedUsr != nil {
invite.UserId = invitedUsr.UserID
}

fullName := invite.Creator.Profile.FullName
if !a.addOrUpdateConfirmation(req.Context(), invite, res) {
return
}
a.logMetric("invite created", req)

if invite.Creator.Profile.Patient.IsOtherPerson {
fullName = invite.Creator.Profile.Patient.FullName
}
if err := a.addProfile(invite); err != nil {
log.Println("SendInvite: ", err.Error())
a.sendModelAsResWithStatus(res, invite, http.StatusOK)
}

var webPath = "signup"
fullName := invite.Creator.Profile.FullName

if invite.UserId != "" {
webPath = "login"
}
if invite.Creator.Profile.Patient.IsOtherPerson {
fullName = invite.Creator.Profile.Patient.FullName
}

emailContent := map[string]interface{}{
"CareteamName": fullName,
"Email": invite.Email,
"WebPath": webPath,
}
var webPath = "signup"

if a.createAndSendNotification(req, invite, emailContent) {
a.logMetric("invite sent", req)
}
}
if invite.UserId != "" {
webPath = "login"
}

a.sendModelAsResWithStatus(res, invite, http.StatusOK)
return
}
}
emailContent := map[string]interface{}{
"CareteamName": fullName,
"Email": invite.Email,
"WebPath": webPath,
"Nickname": ib.Nickname,
}

if a.createAndSendNotification(req, invite, emailContent) {
a.logMetric("invite sent", req)
}

a.sendModelAsResWithStatus(res, invite, http.StatusOK)
return
}

func addsAlertingPermissions(existingPerms, newPerms commonClients.Permissions) bool {
return existingPerms["alerting"] == nil && newPerms["alerting"] != nil
}

// Resend a care team invite
Expand Down
Loading

0 comments on commit dd4091d

Please sign in to comment.