Skip to content

Commit 6ddaf88

Browse files
authored
Merge pull request #294 from authorizerdev/fix/sql-unique-phone-constraint
fix(server): unique constraint for phone_number on mssql
2 parents 16c4b8a + f8bcd0f commit 6ddaf88

File tree

4 files changed

+59
-49
lines changed

4 files changed

+59
-49
lines changed

server/db/models/user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type User struct {
2525
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname" dynamo:"nickname"`
2626
Gender *string `json:"gender" bson:"gender" cql:"gender" dynamo:"gender"`
2727
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate" dynamo:"birthdate"`
28-
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number"`
28+
PhoneNumber *string `gorm:"index" json:"phone_number" bson:"phone_number" cql:"phone_number" dynamo:"phone_number"`
2929
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at" dynamo:"phone_number_verified_at"`
3030
Picture *string `json:"picture" bson:"picture" cql:"picture" dynamo:"picture"`
3131
Roles string `json:"roles" bson:"roles" cql:"roles" dynamo:"roles"`

server/db/providers/sql/provider.go

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package sql
22

33
import (
4-
"fmt"
54
"time"
65

76
"github.com/authorizerdev/authorizer/server/constants"
@@ -71,35 +70,43 @@ func NewProvider() (*provider, error) {
7170
return nil, err
7271
}
7372

73+
// For sqlserver, handle uniqueness of phone_number manually via extra db call
74+
// during create and update mutation.
75+
if sqlDB.Migrator().HasConstraint(&models.User{}, "authorizer_users_phone_number_key") {
76+
err = sqlDB.Migrator().DropConstraint(&models.User{}, "authorizer_users_phone_number_key")
77+
logrus.Debug("Failed to drop phone number constraint:", err)
78+
}
79+
7480
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{})
7581
if err != nil {
7682
return nil, err
7783
}
7884

85+
// IMPACT: Request user to manually delete: UQ_phone_number constraint
7986
// unique constraint on phone number does not work with multiple null values for sqlserver
8087
// for more information check https://stackoverflow.com/a/767702
81-
if dbType == constants.DbTypeSqlserver {
82-
var indexInfos []indexInfo
83-
// remove index on phone number if present with different name
84-
res := sqlDB.Raw("SELECT i.name AS index_name, i.type_desc AS index_algorithm, CASE i.is_unique WHEN 1 THEN 'TRUE' ELSE 'FALSE' END AS is_unique, ac.Name AS column_name FROM sys.tables AS t INNER JOIN sys.indexes AS i ON t.object_id = i.object_id INNER JOIN sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id INNER JOIN sys.all_columns AS ac ON ic.object_id = ac.object_id AND ic.column_id = ac.column_id WHERE t.name = 'authorizer_users' AND SCHEMA_NAME(t.schema_id) = 'dbo';").Scan(&indexInfos)
85-
if res.Error != nil {
86-
return nil, res.Error
87-
}
88-
89-
for _, val := range indexInfos {
90-
if val.ColumnName == phoneNumberColumnName && val.IndexName != phoneNumberIndexName {
91-
// drop index & create new
92-
if res := sqlDB.Exec(fmt.Sprintf(`ALTER TABLE authorizer_users DROP CONSTRAINT "%s";`, val.IndexName)); res.Error != nil {
93-
return nil, res.Error
94-
}
95-
96-
// create index
97-
if res := sqlDB.Exec(fmt.Sprintf("CREATE UNIQUE NONCLUSTERED INDEX %s ON authorizer_users(phone_number) WHERE phone_number IS NOT NULL;", phoneNumberIndexName)); res.Error != nil {
98-
return nil, res.Error
99-
}
100-
}
101-
}
102-
}
88+
// if dbType == constants.DbTypeSqlserver {
89+
// var indexInfos []indexInfo
90+
// // remove index on phone number if present with different name
91+
// res := sqlDB.Raw("SELECT i.name AS index_name, i.type_desc AS index_algorithm, CASE i.is_unique WHEN 1 THEN 'TRUE' ELSE 'FALSE' END AS is_unique, ac.Name AS column_name FROM sys.tables AS t INNER JOIN sys.indexes AS i ON t.object_id = i.object_id INNER JOIN sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id INNER JOIN sys.all_columns AS ac ON ic.object_id = ac.object_id AND ic.column_id = ac.column_id WHERE t.name = 'authorizer_users' AND SCHEMA_NAME(t.schema_id) = 'dbo';").Scan(&indexInfos)
92+
// if res.Error != nil {
93+
// return nil, res.Error
94+
// }
95+
96+
// for _, val := range indexInfos {
97+
// if val.ColumnName == phoneNumberColumnName && val.IndexName != phoneNumberIndexName {
98+
// // drop index & create new
99+
// if res := sqlDB.Exec(fmt.Sprintf(`ALTER TABLE authorizer_users DROP CONSTRAINT "%s";`, val.IndexName)); res.Error != nil {
100+
// return nil, res.Error
101+
// }
102+
103+
// // create index
104+
// if res := sqlDB.Exec(fmt.Sprintf("CREATE UNIQUE NONCLUSTERED INDEX %s ON authorizer_users(phone_number) WHERE phone_number IS NOT NULL;", phoneNumberIndexName)); res.Error != nil {
105+
// return nil, res.Error
106+
// }
107+
// }
108+
// }
109+
// }
103110

104111
return &provider{
105112
db: sqlDB,

server/db/providers/sql/user.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package sql
22

33
import (
44
"context"
5+
"fmt"
6+
"strings"
57
"time"
68

79
"github.com/authorizerdev/authorizer/server/constants"
810
"github.com/authorizerdev/authorizer/server/db/models"
911
"github.com/authorizerdev/authorizer/server/graph/model"
1012
"github.com/authorizerdev/authorizer/server/memorystore"
13+
"github.com/authorizerdev/authorizer/server/refs"
1114
"github.com/google/uuid"
1215
"gorm.io/gorm"
1316
"gorm.io/gorm/clause"
@@ -27,6 +30,12 @@ func (p *provider) AddUser(ctx context.Context, user models.User) (models.User,
2730
user.Roles = defaultRoles
2831
}
2932

33+
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
34+
if u, _ := p.GetUserByPhone(ctx, refs.StringValue(user.PhoneNumber)); u != nil {
35+
return user, fmt.Errorf("user with given phone number already exists")
36+
}
37+
}
38+
3039
user.CreatedAt = time.Now().Unix()
3140
user.UpdatedAt = time.Now().Unix()
3241
user.Key = user.ID
@@ -47,6 +56,12 @@ func (p *provider) AddUser(ctx context.Context, user models.User) (models.User,
4756
func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.User, error) {
4857
user.UpdatedAt = time.Now().Unix()
4958

59+
if user.PhoneNumber != nil && strings.TrimSpace(refs.StringValue(user.PhoneNumber)) != "" {
60+
if u, _ := p.GetUserByPhone(ctx, refs.StringValue(user.PhoneNumber)); u != nil && u.ID != user.ID {
61+
return user, fmt.Errorf("user with given phone number already exists")
62+
}
63+
}
64+
5065
result := p.db.Save(&user)
5166

5267
if result.Error != nil {
@@ -141,3 +156,14 @@ func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{},
141156
}
142157
return nil
143158
}
159+
160+
func (p *provider) GetUserByPhone(ctx context.Context, phoneNumber string) (*models.User, error) {
161+
var user *models.User
162+
result := p.db.Where("phone_number = ?", phoneNumber).First(&user)
163+
164+
if result.Error != nil {
165+
return user, result.Error
166+
}
167+
168+
return user, nil
169+
}

server/test/add_email_template_test.go

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) {
5151
assert.Nil(t, emailTemplate)
5252
})
5353

54-
var design string
55-
design = ""
54+
design := ""
5655

5756
for _, eventType := range s.TestInfo.TestEmailTemplateEventTypes {
5857
t.Run("should add email template with empty design for "+eventType, func(t *testing.T) {
@@ -70,29 +69,7 @@ func addEmailTemplateTest(t *testing.T, s TestSetup) {
7069
assert.NoError(t, err)
7170
assert.Equal(t, et.EventName, eventType)
7271
assert.Equal(t, "Test email", et.Subject)
73-
assert.Equal(t, "Test design", et.Design)
74-
})
75-
}
76-
77-
design = "Test design"
78-
79-
for _, eventType := range s.TestInfo.TestEmailTemplateEventTypes {
80-
t.Run("should add email template for "+eventType, func(t *testing.T) {
81-
emailTemplate, err := resolvers.AddEmailTemplateResolver(ctx, model.AddEmailTemplateRequest{
82-
EventName: eventType,
83-
Template: "Test email",
84-
Subject: "Test email",
85-
Design: &design,
86-
})
87-
assert.NoError(t, err)
88-
assert.NotNil(t, emailTemplate)
89-
assert.NotEmpty(t, emailTemplate.Message)
90-
91-
et, err := db.Provider.GetEmailTemplateByEventName(ctx, eventType)
92-
assert.NoError(t, err)
93-
assert.Equal(t, et.EventName, eventType)
94-
assert.Equal(t, "Test email", et.Subject)
95-
assert.Equal(t, "Test design", et.Design)
72+
assert.Equal(t, "", et.Design)
9673
})
9774
}
9875
})

0 commit comments

Comments
 (0)