Skip to content

Commit

Permalink
[BACK-2780] Begin work on new user profile routes by creating a distinct
Browse files Browse the repository at this point in the history
user Profile object.
  • Loading branch information
lostlevels committed Jan 2, 2024
1 parent 2c81e88 commit f58cec1
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 26 deletions.
42 changes: 30 additions & 12 deletions keycloak/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package keycloak

import (
"context"
"errors"
"maps"
"net/http"
"strings"
"time"

"github.com/Nerzal/gocloak/v12"
"github.com/Nerzal/gocloak/v12/pkg/jwx"
"github.com/kelseyhightower/envconfig"
"golang.org/x/oauth2"
"net/http"
"strings"
"time"

"github.com/pkg/errors"
"github.com/tidepool-org/shoreline/profile"
)

const (
Expand Down Expand Up @@ -56,7 +59,8 @@ type User struct {
}

type UserAttributes struct {
TermsAcceptedDate []string `json:"terms_and_conditions,omitempty"`
TermsAcceptedDate string `json:"terms_and_conditions,omitempty"`
Profile *profile.UserProfile `json:"profile,omitempty"`
}

func NewKeycloakUser(gocloakUser *gocloak.User) *User {
Expand All @@ -73,8 +77,12 @@ func NewKeycloakUser(gocloakUser *gocloak.User) *User {
Enabled: safePBool(gocloakUser.Enabled),
}
if gocloakUser.Attributes != nil {
if ts, ok := (*gocloakUser.Attributes)["terms_and_conditions"]; ok {
user.Attributes.TermsAcceptedDate = ts
attrs := maps.Clone(*gocloakUser.Attributes)
if ts, ok := attrs["terms_and_conditions"]; ok && len(ts) > 0 {
user.Attributes.TermsAcceptedDate = ts[0]
}
if prof, ok := profile.FromAttributes(attrs); ok {
user.Attributes.Profile = prof
}
}

Expand Down Expand Up @@ -267,9 +275,15 @@ func (c *client) UpdateUser(ctx context.Context, user *User) error {
Email: &user.Email,
}

gocloakUser.Attributes = &map[string][]string{
"terms_and_conditions": user.Attributes.TermsAcceptedDate,
attrs := map[string][]string{}
if len(user.Attributes.TermsAcceptedDate) > 0 {
attrs["terms_and_conditions"] = []string{user.Attributes.TermsAcceptedDate}
}
if user.Attributes.Profile != nil {
maps.Copy(attrs, user.Attributes.Profile.ToAttributes())
}

gocloakUser.Attributes = &attrs

if err := c.keycloak.UpdateUser(ctx, token.AccessToken, c.cfg.Realm, gocloakUser); err != nil {
return err
Expand Down Expand Up @@ -310,10 +324,14 @@ func (c *client) CreateUser(ctx context.Context, user *User) (*User, error) {
RealmRoles: &user.Roles,
}

attrs := map[string][]string{}
if len(user.Attributes.TermsAcceptedDate) > 0 {
attrs := map[string][]string{
"terms_and_conditions": user.Attributes.TermsAcceptedDate,
}
attrs["terms_and_conditions"] = []string{user.Attributes.TermsAcceptedDate}
}
if user.Attributes.Profile != nil {
maps.Copy(attrs, user.Attributes.Profile.ToAttributes())
}
if len(attrs) > 0 {
model.Attributes = &attrs
}

Expand Down
125 changes: 125 additions & 0 deletions profile/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package profile

import (
"slices"
)

type UserProfile struct {
FullName string `json:"fullName"`
Patient *PatientProfile `json:"patient,omitempty"`
Clinic *ClinicProfile `json:"clinic,omitempty"`
}

type PatientProfile struct {
Birthday string `json:"birthday"`
DiagnosisDate string `json:"diagnosisDate"`
DiagnosisType string `json:"diagnosisType"`
TargetDevices []string `json:"targetDevices"`
TargetTimezone string `json:"targetTimezone"`
About string `json:"about"`
}

type ClinicProfile struct {
Name string `json:"diagnosisDate"`
Role []string `json:"role"`
Telephone string `json:"telephone"`
}

func (u *UserProfile) ToAttributes() map[string][]string {
attributes := map[string][]string{}

addAttribute(attributes, "profile.fullName", u.FullName)
if u.Patient != nil {
patient := u.Patient
addAttribute(attributes, "profile.patient.birthday", patient.Birthday)
addAttribute(attributes, "profile.patient.diagnosisDate", patient.DiagnosisDate)
addAttribute(attributes, "profile.patient.diagnosisType", patient.DiagnosisType)
addAttributes(attributes, "profile.patient.targetDevices", patient.TargetDevices...)
addAttribute(attributes, "profile.patient.targetTimezone", patient.TargetTimezone)
addAttribute(attributes, "profile.patient.about", patient.About)
}

if u.Clinic != nil {
clinic := u.Clinic
addAttribute(attributes, "profile.clinic.name", clinic.Name)
addAttributes(attributes, "profile.clinic.role", clinic.Role...)
addAttribute(attributes, "profile.clinic.telephone", clinic.Telephone)
}

return attributes
}

func FromAttributes(attributes map[string][]string) (profile *UserProfile, ok bool) {
u := &UserProfile{}
u.FullName = getAttribute(attributes, "profile.fullName")

if containsAnyAttributeKeys(attributes, "profile.patient.birthday", "profile.patient.diagnosisDate", "profile.patient.diagnosisType", "profile.patient.targetDevices", "profile.patient.targetTimezone", "profile.patient.about") {
patient := &PatientProfile{}
patient.Birthday = getAttribute(attributes, "profile.patient.birthday")
patient.DiagnosisDate = getAttribute(attributes, "profile.patient.diagnosisDate")
patient.DiagnosisType = getAttribute(attributes, "profile.patient.diagnosisType")
patient.TargetDevices = getAttributes(attributes, "profile.patient.targetDevices")
patient.TargetTimezone = getAttribute(attributes, "profile.patient.targetTimezone")
patient.About = getAttribute(attributes, "profile.patient.about")
u.Patient = patient
}

if containsAnyAttributeKeys(attributes, "profile.clinic.name", "profile.clinic.role", "profile.clinic.telephone") {
clinic := &ClinicProfile{}
clinic.Name = getAttribute(attributes, "profile.clinic.name")
clinic.Role = getAttributes(attributes, "profile.clinic.role")
clinic.Telephone = getAttribute(attributes, "profile.clinic.telephone")
u.Clinic = clinic
}

if u.Clinic == nil && u.Patient == nil {
return nil, false
}
return u, true
}

func addAttribute(attributes map[string][]string, attribute, value string) (ok bool) {
if !containsAttribute(attributes, attribute, value) {
attributes[attribute] = append(attributes[attribute], value)
return true
}
return false
}

func getAttribute(attributes map[string][]string, attribute string) string {
if len(attributes[attribute]) > 0 {
return attributes[attribute][0]
}
return ""
}

func getAttributes(attributes map[string][]string, attribute string) []string {
return attributes[attribute]
}

func addAttributes(attributes map[string][]string, attribute string, values ...string) (ok bool) {
for _, value := range values {
if addAttribute(attributes, attribute, value) {
ok = true
}
}
return true
}

func containsAttribute(attributes map[string][]string, attribute, value string) bool {
for key, vals := range attributes {
if key == attribute && slices.Contains(vals, value) {
return true
}
}
return false
}

func containsAnyAttributeKeys(attributes map[string][]string, keys ...string) bool {
for key, vals := range attributes {
if len(vals) > 0 && slices.Contains(keys, key) {
return true
}
}
return false
}
9 changes: 5 additions & 4 deletions user/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
api "github.com/tidepool-org/clinic/client"
"github.com/tidepool-org/shoreline/keycloak"
"golang.org/x/oauth2"
"log"
"net/http"
"reflect"
Expand All @@ -16,6 +13,10 @@ import (
"strings"
"time"

api "github.com/tidepool-org/clinic/client"
"github.com/tidepool-org/shoreline/keycloak"
"golang.org/x/oauth2"

"github.com/gorilla/mux"

"github.com/tidepool-org/go-common/clients"
Expand Down Expand Up @@ -401,7 +402,7 @@ func (a *Api) UpdateUser(res http.ResponseWriter, req *http.Request, vars map[st
} else if (updateUserDetails.Roles != nil || updateUserDetails.EmailVerified != nil) && !tokenData.IsServer {
a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, "User does not have permissions")

} else if (updateUserDetails.Password != nil || updateUserDetails.TermsAccepted != nil) && permissions["root"] == nil {
} else if (updateUserDetails.Password != nil || updateUserDetails.TermsAccepted != nil || updateUserDetails.Profile != nil) && permissions["root"] == nil {
a.sendError(res, http.StatusUnauthorized, STATUS_UNAUTHORIZED, "User does not have permissions")
} else {
if updateUserDetails.Password != nil {
Expand Down
11 changes: 7 additions & 4 deletions user/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func firstStringNotEmpty(strs ...string) string {
return ""
}

//Docode the http.Request parsing out the user details
// Docode the http.Request parsing out the user details
func getGivenDetail(req *http.Request) (d map[string]string) {
if req.ContentLength > 0 {
if err := json.NewDecoder(req.Body).Decode(&d); err != nil {
Expand Down Expand Up @@ -63,7 +63,7 @@ func sendModelAsResWithStatus(res http.ResponseWriter, model interface{}, status
}
}

//send metric
// send metric
func (a *Api) logMetric(name, token string, params map[string]string) {
if token == "" {
a.logger.Println("Missing token so couldn't log metric")
Expand All @@ -75,7 +75,7 @@ func (a *Api) logMetric(name, token string, params map[string]string) {
return
}

//send metric
// send metric
func (a *Api) logMetricAsServer(name, token string, params map[string]string) {
if token == "" {
a.logger.Println("Missing token so couldn't log metric")
Expand All @@ -87,7 +87,7 @@ func (a *Api) logMetricAsServer(name, token string, params map[string]string) {
return
}

//send metric
// send metric
func (a *Api) logMetricForUser(id, name, token string, params map[string]string) {
if token == "" {
a.logger.Println("Missing token so couldn't log metric")
Expand Down Expand Up @@ -140,5 +140,8 @@ func (a *Api) asSerializableUser(user *User, isServerRequest bool) interface{} {
if isServerRequest {
serializable["passwordExists"] = (user.PwHash != "")
}
if user.Profile != nil {
serializable["profile"] = *user.Profile
}
return serializable
}
7 changes: 4 additions & 3 deletions user/migrationStore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"context"
"errors"
"fmt"
"github.com/tidepool-org/shoreline/keycloak"
"time"

"github.com/tidepool-org/shoreline/keycloak"
)

var ErrUserConflict = errors.New("user already exists")
Expand Down Expand Up @@ -54,7 +55,7 @@ func (m *MigrationStore) CreateUser(details *NewUserDetails) (*User, error) {
// Automatically set terms accepted date when email is verified (i.e. internal usage only).
termsAccepted := fmt.Sprintf("%v", time.Now().Unix())
keycloakUser.Attributes = keycloak.UserAttributes{
TermsAcceptedDate: []string{termsAccepted},
TermsAcceptedDate: termsAccepted,
}
}

Expand Down Expand Up @@ -165,7 +166,7 @@ func (m *MigrationStore) updateKeycloakUser(user *User, details *UpdateUserDetai
}
if details.TermsAccepted != nil && IsValidTimestamp(*details.TermsAccepted) {
if ts, err := TimestampToUnixString(*details.TermsAccepted); err == nil {
keycloakUser.Attributes.TermsAcceptedDate = []string{ts}
keycloakUser.Attributes.TermsAcceptedDate = ts
}
}

Expand Down
10 changes: 7 additions & 3 deletions user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/tidepool-org/shoreline/keycloak"
"io"
"math/rand"
"regexp"
"strconv"
"strings"
"time"

"github.com/tidepool-org/shoreline/keycloak"
"github.com/tidepool-org/shoreline/profile"
)

const (
Expand Down Expand Up @@ -56,6 +58,7 @@ type User struct {
ModifiedUserID string `json:"modifiedUserId,omitempty" bson:"modifiedUserId,omitempty"`
DeletedTime string `json:"deletedTime,omitempty" bson:"deletedTime,omitempty"`
DeletedUserID string `json:"deletedUserId,omitempty" bson:"deletedUserId,omitempty"`
Profile *profile.UserProfile `json:"-" bson:"-"`
}

/*
Expand All @@ -82,6 +85,7 @@ type UpdateUserDetails struct {
Roles []string
TermsAccepted *string
EmailVerified *bool
Profile *profile.UserProfile
}

type Profile struct {
Expand Down Expand Up @@ -645,7 +649,7 @@ func (u *User) ToKeycloakUser() *keycloak.User {
keycloakUser.Roles = append(keycloakUser.Roles, RoleCustodialAccount)
}
if termsAccepted, err := TimestampToUnixString(u.TermsAccepted); err == nil {
keycloakUser.Attributes.TermsAcceptedDate = []string{termsAccepted}
keycloakUser.Attributes.TermsAcceptedDate = termsAccepted
}

return keycloakUser
Expand All @@ -662,7 +666,7 @@ func (u *User) IsEnabled() bool {
func NewUserFromKeycloakUser(keycloakUser *keycloak.User) *User {
termsAcceptedDate := ""
if len(keycloakUser.Attributes.TermsAcceptedDate) > 0 {
if ts, err := UnixStringToTimestamp(keycloakUser.Attributes.TermsAcceptedDate[0]); err == nil {
if ts, err := UnixStringToTimestamp(keycloakUser.Attributes.TermsAcceptedDate); err == nil {
termsAcceptedDate = ts
}
}
Expand Down

0 comments on commit f58cec1

Please sign in to comment.