package graphql import ( "context" "fmt" "github.com/Nerzal/gocloak/v11" "github.com/golang-jwt/jwt/v4" "github.com/gshopify/service-wrapper/auth" "github.com/gshopify/service-wrapper/config" "github.com/mitchellh/mapstructure" "gshopper.com/gshopify/customer/graphql/generated" m "gshopper.com/gshopify/customer/model" "sync" "time" ) type Resolver struct { conf *auth.Config client gocloak.GoCloak mu sync.Mutex _token *gocloak.JWT _expiresAt time.Time } func NewResolver() (*Resolver, error) { r := &Resolver{ conf: auth.New(), } if err := config.Instance().Load(context.Background(), r.conf); err != nil { return nil, err } r.client = gocloak.NewClient(r.conf.Endpoint) return r, nil } func (r *Resolver) decodeAccessToken(ctx context.Context, t string) (*jwt.Token, string, error) { if t == "" { return nil, "", fmt.Errorf("could not decode accessToken: Token is empty") } token, claim, err := r.client.DecodeAccessToken(ctx, t, r.conf.Cli.Realm) if err != nil { return nil, "", err } if !token.Valid { return nil, "", fmt.Errorf("could not decode accessToken: Token is NOT valid") } var sessionId string if claimed, ok := (*claim)["sid"]; ok { if s, ok := claimed.(string); ok { sessionId = s } } if sessionId == "" { return nil, "", fmt.Errorf("could not claim session id") } return token, sessionId, nil } func (r *Resolver) customer(ctx context.Context, t string) (*generated.Customer, error) { var ( customer = generated.Customer{} udata map[string]any phone *m.Phone err error ) udata, err = r.client.GetRawUserInfo(ctx, t, r.conf.Cli.Realm) if err != nil { return nil, err } if err = mapstructure.Decode(udata, &customer); err != nil { return nil, err } customer.Phone = nil phone, err = m.ParsePhoneNumber(udata) if err != nil { return nil, err } customer.Phone = gocloak.StringP(phone.String()) customer.Metafields = append(customer.Metafields, &generated.Metafield{ Namespace: "customer", Key: "phone_region", Type: "single_line_text_field", Value: phone.PhoneRegion, }, &generated.Metafield{ Namespace: "customer", Key: "phone_verified", Type: "boolean", Value: fmt.Sprintf("%v", phone.Verified), }) return &customer, nil } func (r *Resolver) admin(ctx context.Context) (*gocloak.JWT, error) { var ( creds = r.conf.Admin err error ) if time.Now().After(r._expiresAt) { r.mu.Lock() defer r.mu.Unlock() if r._token != nil { r._token, err = r.client.RefreshToken(ctx, r._token.RefreshToken, creds.ClientId, creds.ClientSecret, creds.Realm) } if r._token == nil || err != nil { r._token, err = r.client.LoginAdmin(ctx, creds.Login, creds.Password, creds.Realm) } if r._token == nil || err != nil { r._expiresAt = time.Time{} } else { r._expiresAt = time.Now(). Add(time.Duration(r._token.ExpiresIn) * time.Second). Add(-time.Second * 5) } } return r._token, err } func (r *Resolver) saveSession(ctx context.Context, token *gocloak.JWT) error { return auth.SessionManager().PutToken( ctx, token.SessionState, token.RefreshToken, time.Duration(token.RefreshExpiresIn)*time.Second) } // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }