package models import ( "database/sql" "strings" "time" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/supabase/auth/internal/storage" ) type Identity struct { // returned as identity_id in JSON for backward compatibility with the interface exposed by the client library // see https://github.com/supabase/gotrue-js/blob/c9296bbc27a2f036af55c1f33fca5930704bd021/src/lib/types.ts#L230-L240 ID uuid.UUID `json:"identity_id" db:"id"` // returned as id in JSON for backward compatibility with the interface exposed by the client library // see https://github.com/supabase/gotrue-js/blob/c9296bbc27a2f036af55c1f33fca5930704bd021/src/lib/types.ts#L230-L240 ProviderID string `json:"id" db:"provider_id"` UserID uuid.UUID `json:"user_id" db:"user_id"` IdentityData JSONMap `json:"identity_data,omitempty" db:"identity_data"` Provider string `json:"provider" db:"provider"` LastSignInAt *time.Time `json:"last_sign_in_at,omitempty" db:"last_sign_in_at"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` Email storage.NullString `json:"email,omitempty" db:"email" rw:"r"` } func (Identity) TableName() string { tableName := "identities" return tableName } // GetEmail returns the user's email as a string func (i *Identity) GetEmail() string { return string(i.Email) } // NewIdentity returns an identity associated to the user's id. func NewIdentity(user *User, provider string, identityData map[string]interface{}) (*Identity, error) { providerId, ok := identityData["sub"] if !ok { return nil, errors.New("error missing provider id") } now := time.Now() identity := &Identity{ ProviderID: providerId.(string), UserID: user.ID, IdentityData: identityData, Provider: provider, LastSignInAt: &now, } if email, ok := identityData["email"]; ok { identity.Email = storage.NullString(email.(string)) } return identity, nil } func (i *Identity) BeforeCreate(tx *pop.Connection) error { return i.BeforeUpdate(tx) } func (i *Identity) BeforeUpdate(tx *pop.Connection) error { if _, ok := i.IdentityData["email"]; ok { i.IdentityData["email"] = strings.ToLower(i.IdentityData["email"].(string)) } return nil } func (i *Identity) IsForSSOProvider() bool { return strings.HasPrefix(i.Provider, "sso:") } // FindIdentityById searches for an identity with the matching id and provider given. func FindIdentityByIdAndProvider(tx *storage.Connection, providerId, provider string) (*Identity, error) { identity := &Identity{} if err := tx.Q().Where("provider_id = ? AND provider = ?", providerId, provider).First(identity); err != nil { if errors.Cause(err) == sql.ErrNoRows { return nil, IdentityNotFoundError{} } return nil, errors.Wrap(err, "error finding identity") } return identity, nil } // FindIdentitiesByUserID returns all identities associated to a user ID. func FindIdentitiesByUserID(tx *storage.Connection, userID uuid.UUID) ([]*Identity, error) { identities := []*Identity{} if err := tx.Q().Where("user_id = ?", userID).All(&identities); err != nil { if errors.Cause(err) == sql.ErrNoRows { return identities, nil } return nil, errors.Wrap(err, "error finding identities") } return identities, nil } // FindProvidersByUser returns all providers associated to a user func FindProvidersByUser(tx *storage.Connection, user *User) ([]string, error) { identities := []Identity{} providerExists := map[string]bool{} providers := make([]string, 0) if err := tx.Q().Select("provider").Where("user_id = ?", user.ID).All(&identities); err != nil { if errors.Cause(err) == sql.ErrNoRows { return providers, nil } return nil, errors.Wrap(err, "error finding providers") } for _, identity := range identities { if _, ok := providerExists[identity.Provider]; !ok { providers = append(providers, identity.Provider) providerExists[identity.Provider] = true } } return providers, nil } // UpdateIdentityData sets all identity_data from a map of updates, // ensuring that it doesn't override attributes that are not // in the provided map. func (i *Identity) UpdateIdentityData(tx *storage.Connection, updates map[string]interface{}) error { if i.IdentityData == nil { i.IdentityData = updates } else { for key, value := range updates { if value != nil { i.IdentityData[key] = value } else { delete(i.IdentityData, key) } } } // pop doesn't support updates on tables with composite primary keys so we use a raw query here. return tx.RawQuery( "update "+(&pop.Model{Value: Identity{}}).TableName()+" set identity_data = ? where id = ?", i.IdentityData, i.ID, ).Exec() }