Merge pull request #206 from authorizerdev/feat/2fa

feat: add mutifactor authentication
This commit is contained in:
Lakhan Samani
2022-08-07 11:11:56 +05:30
committed by GitHub
63 changed files with 2300 additions and 322 deletions

View File

@@ -9,6 +9,7 @@ type CollectionList struct {
Webhook string
WebhookLog string
EmailTemplate string
OTP string
}
var (
@@ -23,5 +24,6 @@ var (
Webhook: Prefix + "webhooks",
WebhookLog: Prefix + "webhook_logs",
EmailTemplate: Prefix + "email_templates",
OTP: Prefix + "otps",
}
)

12
server/db/models/otp.go Normal file
View File

@@ -0,0 +1,12 @@
package models
// OTP model for database
type OTP struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
Otp string `json:"otp" bson:"otp" cql:"otp"`
ExpiresAt int64 `json:"expires_at" bson:"expires_at" cql:"expires_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
}

View File

@@ -14,51 +14,53 @@ type User struct {
Key string `json:"_key,omitempty" bson:"_key,omitempty" cql:"_key,omitempty"` // for arangodb
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id" cql:"id"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"`
Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
Gender *string `json:"gender" bson:"gender" cql:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"`
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
Email string `gorm:"unique" json:"email" bson:"email" cql:"email"`
EmailVerifiedAt *int64 `json:"email_verified_at" bson:"email_verified_at" cql:"email_verified_at"`
Password *string `gorm:"type:text" json:"password" bson:"password" cql:"password"`
SignupMethods string `json:"signup_methods" bson:"signup_methods" cql:"signup_methods"`
GivenName *string `json:"given_name" bson:"given_name" cql:"given_name"`
FamilyName *string `json:"family_name" bson:"family_name" cql:"family_name"`
MiddleName *string `json:"middle_name" bson:"middle_name" cql:"middle_name"`
Nickname *string `json:"nickname" bson:"nickname" cql:"nickname"`
Gender *string `json:"gender" bson:"gender" cql:"gender"`
Birthdate *string `json:"birthdate" bson:"birthdate" cql:"birthdate"`
PhoneNumber *string `gorm:"unique" json:"phone_number" bson:"phone_number" cql:"phone_number"`
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at" cql:"phone_number_verified_at"`
Picture *string `gorm:"type:text" json:"picture" bson:"picture" cql:"picture"`
Roles string `json:"roles" bson:"roles" cql:"roles"`
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp" cql:"revoked_timestamp"`
IsMultiFactorAuthEnabled *bool `json:"is_multi_factor_auth_enabled" bson:"is_multi_factor_auth_enabled" cql:"is_multi_factor_auth_enabled"`
UpdatedAt int64 `json:"updated_at" bson:"updated_at" cql:"updated_at"`
CreatedAt int64 `json:"created_at" bson:"created_at" cql:"created_at"`
}
func (user *User) AsAPIUser() *model.User {
isEmailVerified := user.EmailVerifiedAt != nil
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
id := user.ID
if strings.Contains(id, Collections.WebhookLog+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/")
}
// id := user.ID
// if strings.Contains(id, Collections.User+"/") {
// id = strings.TrimPrefix(id, Collections.User+"/")
// }
return &model.User{
ID: id,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: refs.NewStringRef(user.Email),
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp,
CreatedAt: refs.NewInt64Ref(user.CreatedAt),
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt),
ID: user.ID,
Email: user.Email,
EmailVerified: isEmailVerified,
SignupMethods: user.SignupMethods,
GivenName: user.GivenName,
FamilyName: user.FamilyName,
MiddleName: user.MiddleName,
Nickname: user.Nickname,
PreferredUsername: refs.NewStringRef(user.Email),
Gender: user.Gender,
Birthdate: user.Birthdate,
PhoneNumber: user.PhoneNumber,
PhoneNumberVerified: &isPhoneVerified,
Picture: user.Picture,
Roles: strings.Split(user.Roles, ","),
RevokedTimestamp: user.RevokedTimestamp,
IsMultiFactorAuthEnabled: user.IsMultiFactorAuthEnabled,
CreatedAt: refs.NewInt64Ref(user.CreatedAt),
UpdatedAt: refs.NewInt64Ref(user.UpdatedAt),
}
}

View File

@@ -25,8 +25,8 @@ type VerificationRequest struct {
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
id := v.ID
if strings.Contains(id, Collections.WebhookLog+"/") {
id = strings.TrimPrefix(id, Collections.WebhookLog+"/")
if strings.Contains(id, Collections.VerificationRequest+"/") {
id = strings.TrimPrefix(id, Collections.VerificationRequest+"/")
}
return &model.VerificationRequest{

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddEmailTemplate(ctx context.Context, emailTemplate models.EmailTemplate) (*model.EmailTemplate, error) {
if emailTemplate.ID == "" {
emailTemplate.ID = uuid.New().String()
emailTemplate.Key = emailTemplate.ID
}
emailTemplate.Key = emailTemplate.ID

View File

@@ -15,6 +15,7 @@ import (
func (p *provider) AddEnv(ctx context.Context, env models.Env) (models.Env, error) {
if env.ID == "" {
env.ID = uuid.New().String()
env.Key = env.ID
}
env.CreatedAt = time.Now().Unix()

View File

@@ -0,0 +1,92 @@
package arangodb
import (
"context"
"fmt"
"time"
"github.com/arangodb/go-driver"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
var meta driver.DocumentMeta
var err error
if shouldCreate {
meta, err = otpCollection.CreateDocument(ctx, otp)
} else {
meta, err = otpCollection.UpdateDocument(ctx, otp.Key, otp)
}
if err != nil {
return nil, err
}
otp.Key = meta.Key
otp.ID = meta.ID.String()
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf("FOR d in %s FILTER d.email == @email RETURN d", models.Collections.OTP)
bindVars := map[string]interface{}{
"email": emailAddress,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
return nil, err
}
defer cursor.Close()
for {
if !cursor.HasMore() {
if otp.Key == "" {
return nil, fmt.Errorf("email template not found")
}
break
}
_, err := cursor.ReadDocument(ctx, &otp)
if err != nil {
return nil, err
}
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection, _ := p.db.Collection(ctx, models.Collections.OTP)
_, err := otpCollection.RemoveDocument(ctx, otp.ID)
if err != nil {
return err
}
return nil
}

View File

@@ -148,6 +148,20 @@ func NewProvider() (*provider, error) {
Sparse: true,
})
otpCollectionExists, err := arangodb.CollectionExists(ctx, models.Collections.OTP)
if !otpCollectionExists {
_, err = arangodb.CreateCollection(ctx, models.Collections.OTP, nil)
if err != nil {
return nil, err
}
}
otpCollection, _ := arangodb.Collection(nil, models.Collections.OTP)
otpCollection.EnsureHashIndex(ctx, []string{"email"}, &arangoDriver.EnsureHashIndexOptions{
Unique: true,
Sparse: true,
})
return &provider{
db: arangodb,
}, err

View File

@@ -12,6 +12,7 @@ import (
func (p *provider) AddSession(ctx context.Context, session models.Session) error {
if session.ID == "" {
session.ID = uuid.New().String()
session.Key = session.ID
}
session.CreatedAt = time.Now().Unix()

View File

@@ -2,22 +2,26 @@ package arangodb
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/arangodb/go-driver"
arangoDriver "github.com/arangodb/go-driver"
"github.com/google/uuid"
"github.com/authorizerdev/authorizer/server/constants"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
)
// AddUser to save user information in database
func (p *provider) AddUser(ctx context.Context, user models.User) (models.User, error) {
if user.ID == "" {
user.ID = uuid.New().String()
user.Key = user.ID
}
if user.Roles == "" {
@@ -65,7 +69,7 @@ func (p *provider) DeleteUser(ctx context.Context, user models.User) error {
query := fmt.Sprintf(`FOR d IN %s FILTER d.user_id == @user_id REMOVE { _key: d._key } IN %s`, models.Collections.Session, models.Collections.Session)
bindVars := map[string]interface{}{
"user_id": user.ID,
"user_id": user.Key,
}
cursor, err := p.db.Query(ctx, query, bindVars)
if err != nil {
@@ -174,3 +178,36 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil
}
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userInfoBytes, err := json.Marshal(data)
if err != nil {
return err
}
query := ""
if ids != nil && len(ids) > 0 {
keysArray := ""
for _, id := range ids {
keysArray += fmt.Sprintf("'%s', ", id)
}
keysArray = strings.Trim(keysArray, " ")
keysArray = strings.TrimSuffix(keysArray, ",")
query = fmt.Sprintf("FOR u IN %s FILTER u._id IN [%s] UPDATE u._key with %s IN %s", models.Collections.User, keysArray, string(userInfoBytes), models.Collections.User)
} else {
query = fmt.Sprintf("FOR u IN %s UPDATE u._key with %s IN %s", models.Collections.User, string(userInfoBytes), models.Collections.User)
}
_, err = p.db.Query(ctx, query, nil)
if err != nil {
return err
}
return nil
}

View File

@@ -15,6 +15,7 @@ import (
func (p *provider) AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
if verificationRequest.ID == "" {
verificationRequest.ID = uuid.New().String()
verificationRequest.Key = verificationRequest.ID
}
verificationRequest.CreatedAt = time.Now().Unix()

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhook(ctx context.Context, webhook models.Webhook) (*model.Webhook, error) {
if webhook.ID == "" {
webhook.ID = uuid.New().String()
webhook.Key = webhook.ID
}
webhook.Key = webhook.ID

View File

@@ -16,6 +16,7 @@ import (
func (p *provider) AddWebhookLog(ctx context.Context, webhookLog models.WebhookLog) (*model.WebhookLog, error) {
if webhookLog.ID == "" {
webhookLog.ID = uuid.New().String()
webhookLog.Key = webhookLog.ID
}
webhookLog.Key = webhookLog.ID

View File

@@ -0,0 +1,67 @@
package cassandradb
import (
"context"
"fmt"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/gocql/gocql"
"github.com/google/uuid"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
shouldCreate = true
otp = &models.OTP{
ID: uuid.NewString(),
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
}
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
query := ""
if shouldCreate {
query = fmt.Sprintf(`INSERT INTO %s (id, email, otp, expires_at, created_at, updated_at) VALUES ('%s', '%s', '%s', %d, %d, %d)`, KeySpace+"."+models.Collections.OTP, otp.ID, otp.Email, otp.Otp, otp.ExpiresAt, otp.CreatedAt, otp.UpdatedAt)
} else {
query = fmt.Sprintf(`UPDATE %s SET otp = '%s', expires_at = %d, updated_at = %d WHERE id = '%s'`, KeySpace+"."+models.Collections.OTP, otp.Otp, otp.ExpiresAt, otp.UpdatedAt, otp.ID)
}
err := p.db.Query(query).Exec()
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
query := fmt.Sprintf(`SELECT id, email, otp, expires_at, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING`, KeySpace+"."+models.Collections.OTP, emailAddress)
err := p.db.Query(query).Consistency(gocql.One).Scan(&otp.ID, &otp.Email, &otp.Otp, &otp.ExpiresAt, &otp.CreatedAt, &otp.UpdatedAt)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
query := fmt.Sprintf("DELETE FROM %s WHERE id = '%s'", KeySpace+"."+models.Collections.OTP, otp.ID)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
return nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/gocql/gocql"
cansandraDriver "github.com/gocql/gocql"
log "github.com/sirupsen/logrus"
)
type provider struct {
@@ -99,6 +100,7 @@ func NewProvider() (*provider, error) {
cassandraClient.Consistency = gocql.LocalQuorum
cassandraClient.ConnectTimeout = 10 * time.Second
cassandraClient.ProtoVersion = 4
cassandraClient.Timeout = 30 * time.Minute // for large data
session, err := cassandraClient.CreateSession()
if err != nil {
@@ -159,6 +161,13 @@ func NewProvider() (*provider, error) {
if err != nil {
return nil, err
}
// add is_multi_factor_auth_enabled on users table
userTableAlterQuery := fmt.Sprintf(`ALTER TABLE %s.%s ADD is_multi_factor_auth_enabled boolean`, KeySpace, models.Collections.User)
err = session.Query(userTableAlterQuery).Exec()
if err != nil {
log.Debug("Failed to alter table as column exists: ", err)
// return nil, err
}
// token is reserved keyword in cassandra, hence we need to use jwt_token
verificationRequestCollectionQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, jwt_token text, identifier text, expires_at bigint, email text, nonce text, redirect_uri text, created_at bigint, updated_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.VerificationRequest)
@@ -221,6 +230,17 @@ func NewProvider() (*provider, error) {
return nil, err
}
otpCollection := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s (id text, email text, otp text, expires_at bigint, updated_at bigint, created_at bigint, PRIMARY KEY (id))", KeySpace, models.Collections.OTP)
err = session.Query(otpCollection).Exec()
if err != nil {
return nil, err
}
otpIndexQuery := fmt.Sprintf("CREATE INDEX IF NOT EXISTS authorizer_otp_email ON %s.%s (email)", KeySpace, models.Collections.OTP)
err = session.Query(otpIndexQuery).Exec()
if err != nil {
return nil, err
}
return &provider{
db: session,
}, err

View File

@@ -107,7 +107,7 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
}
if value == nil {
updateFields += fmt.Sprintf("%s = null,", key)
updateFields += fmt.Sprintf("%s = null, ", key)
continue
}
@@ -122,7 +122,6 @@ func (p *provider) UpdateUser(ctx context.Context, user models.User) (models.Use
updateFields = strings.TrimSuffix(updateFields, ",")
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = '%s'", KeySpace+"."+models.Collections.User, updateFields, user.ID)
err = p.db.Query(query).Exec()
if err != nil {
return user, err
@@ -173,14 +172,14 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// there is no offset in cassandra
// so we fetch till limit + offset
// and return the results from offset to limit
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s LIMIT %d", KeySpace+"."+models.Collections.User, pagination.Limit+pagination.Offset)
scanner := p.db.Query(query).Iter().Scanner()
counter := int64(0)
for scanner.Next() {
if counter >= pagination.Offset {
var user models.User
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
err := scanner.Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return nil, err
}
@@ -197,8 +196,8 @@ func (p *provider) ListUsers(ctx context.Context, pagination model.Pagination) (
// GetUserByEmail to get user information from database using email address
func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.User, error) {
var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE email = '%s' LIMIT 1 ALLOW FILTERING", KeySpace+"."+models.Collections.User, email)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return user, err
}
@@ -208,10 +207,95 @@ func (p *provider) GetUserByEmail(ctx context.Context, email string) (models.Use
// GetUserByID to get user information from database using user ID
func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, error) {
var user models.User
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.CreatedAt, &user.UpdatedAt)
query := fmt.Sprintf("SELECT id, email, email_verified_at, password, signup_methods, given_name, family_name, middle_name, nickname, birthdate, phone_number, phone_number_verified_at, picture, roles, revoked_timestamp, is_multi_factor_auth_enabled, created_at, updated_at FROM %s WHERE id = '%s' LIMIT 1", KeySpace+"."+models.Collections.User, id)
err := p.db.Query(query).Consistency(gocql.One).Scan(&user.ID, &user.Email, &user.EmailVerifiedAt, &user.Password, &user.SignupMethods, &user.GivenName, &user.FamilyName, &user.MiddleName, &user.Nickname, &user.Birthdate, &user.PhoneNumber, &user.PhoneNumberVerifiedAt, &user.Picture, &user.Roles, &user.RevokedTimestamp, &user.IsMultiFactorAuthEnabled, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return user, err
}
return user, nil
}
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
updateFields := ""
for key, value := range data {
if key == "_id" {
continue
}
if key == "_key" {
continue
}
if value == nil {
updateFields += fmt.Sprintf("%s = null,", key)
continue
}
valueType := reflect.TypeOf(value)
if valueType.Name() == "string" {
updateFields += fmt.Sprintf("%s = '%s', ", key, value.(string))
} else {
updateFields += fmt.Sprintf("%s = %v, ", key, value)
}
}
updateFields = strings.Trim(updateFields, " ")
updateFields = strings.TrimSuffix(updateFields, ",")
query := ""
if ids != nil && len(ids) > 0 {
idsString := ""
for _, id := range ids {
idsString += fmt.Sprintf("'%s', ", id)
}
idsString = strings.Trim(idsString, " ")
idsString = strings.TrimSuffix(idsString, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idsString)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
} else {
// get all ids
getUserIDsQuery := fmt.Sprintf(`SELECT id FROM %s`, KeySpace+"."+models.Collections.User)
scanner := p.db.Query(getUserIDsQuery).Iter().Scanner()
// only 100 ids are allowed in 1 query
// hence we need create multiple update queries
idsString := ""
idsStringArray := []string{idsString}
counter := 1
for scanner.Next() {
var id string
err := scanner.Scan(&id)
if err == nil {
idsString += fmt.Sprintf("'%s', ", id)
}
counter++
if counter > 100 {
idsStringArray = append(idsStringArray, idsString)
counter = 1
idsString = ""
} else {
// update the last index of array when count is less than 100
idsStringArray[len(idsStringArray)-1] = idsString
}
}
for _, idStr := range idsStringArray {
idStr = strings.Trim(idStr, " ")
idStr = strings.TrimSuffix(idStr, ",")
query = fmt.Sprintf("UPDATE %s SET %s WHERE id IN (%s)", KeySpace+"."+models.Collections.User, updateFields, idStr)
err := p.db.Query(query).Exec()
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,70 @@
package mongodb
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otpParam *models.OTP) (*models.OTP, error) {
otp, _ := p.GetOTPByEmail(ctx, otpParam.Email)
shouldCreate := false
if otp == nil {
id := uuid.NewString()
otp = &models.OTP{
ID: id,
Key: id,
Otp: otpParam.Otp,
Email: otpParam.Email,
ExpiresAt: otpParam.ExpiresAt,
CreatedAt: time.Now().Unix(),
}
shouldCreate = true
} else {
otp.Otp = otpParam.Otp
otp.ExpiresAt = otpParam.ExpiresAt
}
otp.UpdatedAt = time.Now().Unix()
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
var err error
if shouldCreate {
_, err = otpCollection.InsertOne(ctx, otp)
} else {
_, err = otpCollection.UpdateOne(ctx, bson.M{"_id": bson.M{"$eq": otp.ID}}, bson.M{"$set": otp}, options.MergeUpdateOptions())
}
if err != nil {
return nil, err
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
err := otpCollection.FindOne(ctx, bson.M{"email": emailAddress}).Decode(&otp)
if err != nil {
return nil, err
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
otpCollection := p.db.Collection(models.Collections.OTP, options.Collection())
_, err := otpCollection.DeleteOne(nil, bson.M{"_id": otp.ID}, options.Delete())
if err != nil {
return err
}
return nil
}

View File

@@ -110,6 +110,15 @@ func NewProvider() (*provider, error) {
},
}, options.CreateIndexes())
mongodb.CreateCollection(ctx, models.Collections.OTP, options.CreateCollection())
otpCollection := mongodb.Collection(models.Collections.OTP, options.Collection())
otpCollection.Indexes().CreateMany(ctx, []mongo.IndexModel{
{
Keys: bson.M{"email": 1},
Options: options.Index().SetUnique(true).SetSparse(true),
},
}, options.CreateIndexes())
return &provider{
db: mongodb,
}, nil

View File

@@ -9,7 +9,9 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@@ -129,3 +131,27 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil
}
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
userCollection := p.db.Collection(models.Collections.User, options.Collection())
var res *mongo.UpdateResult
var err error
if ids != nil && len(ids) > 0 {
res, err = userCollection.UpdateMany(ctx, bson.M{"_id": bson.M{"$in": ids}}, bson.M{"$set": data})
} else {
res, err = userCollection.UpdateMany(ctx, bson.M{}, bson.M{"$set": data})
}
if err != nil {
return err
} else {
log.Info("Updated users: ", res.ModifiedCount)
}
return nil
}

View File

@@ -0,0 +1,22 @@
package provider_template
import (
"context"
"github.com/authorizerdev/authorizer/server/db/models"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
return nil, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
return nil, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
return nil
}

View File

@@ -60,3 +60,12 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil
}
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
return nil
}

View File

@@ -20,6 +20,9 @@ type Provider interface {
GetUserByEmail(ctx context.Context, email string) (models.User, error)
// GetUserByID to get user information from database using user ID
GetUserByID(ctx context.Context, id string) (models.User, error)
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error
// AddVerification to save verification request in database
AddVerificationRequest(ctx context.Context, verificationRequest models.VerificationRequest) (models.VerificationRequest, error)
@@ -72,4 +75,11 @@ type Provider interface {
GetEmailTemplateByEventName(ctx context.Context, eventName string) (*model.EmailTemplate, error)
// DeleteEmailTemplate to delete EmailTemplate
DeleteEmailTemplate(ctx context.Context, emailTemplate *model.EmailTemplate) error
// UpsertOTP to add or update otp
UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error)
// GetOTPByEmail to get otp for a given email address
GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error)
// DeleteOTP to delete otp
DeleteOTP(ctx context.Context, otp *models.OTP) error
}

View File

@@ -0,0 +1,53 @@
package sql
import (
"context"
"time"
"github.com/authorizerdev/authorizer/server/db/models"
"github.com/google/uuid"
"gorm.io/gorm/clause"
)
// UpsertOTP to add or update otp
func (p *provider) UpsertOTP(ctx context.Context, otp *models.OTP) (*models.OTP, error) {
if otp.ID == "" {
otp.ID = uuid.New().String()
}
otp.Key = otp.ID
otp.CreatedAt = time.Now().Unix()
otp.UpdatedAt = time.Now().Unix()
res := p.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"otp", "expires_at", "updated_at"}),
}).Create(&otp)
if res.Error != nil {
return nil, res.Error
}
return otp, nil
}
// GetOTPByEmail to get otp for a given email address
func (p *provider) GetOTPByEmail(ctx context.Context, emailAddress string) (*models.OTP, error) {
var otp models.OTP
result := p.db.Where("email = ?", emailAddress).First(&otp)
if result.Error != nil {
return nil, result.Error
}
return &otp, nil
}
// DeleteOTP to delete otp
func (p *provider) DeleteOTP(ctx context.Context, otp *models.OTP) error {
result := p.db.Delete(&models.OTP{
ID: otp.ID,
})
if result.Error != nil {
return result.Error
}
return nil
}

View File

@@ -40,6 +40,7 @@ func NewProvider() (*provider, error) {
NamingStrategy: schema.NamingStrategy{
TablePrefix: models.Prefix,
},
AllowGlobalUpdate: true,
}
dbType := memorystore.RequiredEnvStoreObj.GetRequiredEnv().DatabaseType
@@ -60,7 +61,7 @@ func NewProvider() (*provider, error) {
return nil, err
}
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{})
err = sqlDB.AutoMigrate(&models.User{}, &models.VerificationRequest{}, &models.Session{}, &models.Env{}, &models.Webhook{}, models.WebhookLog{}, models.EmailTemplate{}, &models.OTP{})
if err != nil {
return nil, err
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -121,3 +122,22 @@ func (p *provider) GetUserByID(ctx context.Context, id string) (models.User, err
return user, nil
}
// UpdateUsers to update multiple users, with parameters of user IDs slice
// If ids set to nil / empty all the users will be updated
func (p *provider) UpdateUsers(ctx context.Context, data map[string]interface{}, ids []string) error {
// set updated_at time for all users
data["updated_at"] = time.Now().Unix()
var res *gorm.DB
if ids != nil && len(ids) > 0 {
res = p.db.Model(&models.User{}).Where("id in ?", ids).Updates(data)
} else {
res = p.db.Model(&models.User{}).Updates(data)
}
if res.Error != nil {
return res.Error
}
return nil
}