diff --git a/server/constants/constants.go b/server/constants/constants.go index 178080e..666ddf0 100644 --- a/server/constants/constants.go +++ b/server/constants/constants.go @@ -23,6 +23,10 @@ var ( DISABLE_EMAIL_VERIFICATION = "false" DISABLE_BASIC_AUTHENTICATION = "false" + // ROLES + ROLES = []string{} + DEFAULT_ROLE = "" + // OAuth login GOOGLE_CLIENT_ID = "" GOOGLE_CLIENT_SECRET = "" diff --git a/server/db/db.go b/server/db/db.go index 0110322..dbd6350 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -24,6 +24,7 @@ type Manager interface { GetVerificationRequests() ([]VerificationRequest, error) GetVerificationByEmail(email string) (VerificationRequest, error) DeleteUser(email string) error + SaveRoles(roles []Role) error } type manager struct { @@ -53,7 +54,7 @@ func InitDB() { if err != nil { log.Fatal("Failed to init db:", err) } else { - db.AutoMigrate(&User{}, &VerificationRequest{}) + db.AutoMigrate(&User{}, &VerificationRequest{}, &Role{}) } Mgr = &manager{db: db} diff --git a/server/db/roles.go b/server/db/roles.go new file mode 100644 index 0000000..2506ad0 --- /dev/null +++ b/server/db/roles.go @@ -0,0 +1,19 @@ +package db + +import "log" + +type Role struct { + ID uint `gorm:"primaryKey"` + Role string +} + +// SaveRoles function to save roles +func (mgr *manager) SaveRoles(roles []Role) error { + res := mgr.db.Create(&roles) + if res.Error != nil { + log.Println(`Error saving roles`) + return res.Error + } + + return nil +} diff --git a/server/db/user.go b/server/db/user.go index 7a3dd59..ede8434 100644 --- a/server/db/user.go +++ b/server/db/user.go @@ -17,6 +17,7 @@ type User struct { CreatedAt int64 `gorm:"autoCreateTime"` UpdatedAt int64 `gorm:"autoUpdateTime"` Image string + Roles string } // SaveUser function to add user even with email conflict diff --git a/server/env.go b/server/env.go index 0ccfef2..86f0b83 100644 --- a/server/env.go +++ b/server/env.go @@ -73,6 +73,7 @@ func InitEnv() { constants.RESET_PASSWORD_URL = strings.TrimPrefix(os.Getenv("RESET_PASSWORD_URL"), "/") constants.DISABLE_BASIC_AUTHENTICATION = os.Getenv("DISABLE_BASIC_AUTHENTICATION") constants.DISABLE_EMAIL_VERIFICATION = os.Getenv("DISABLE_EMAIL_VERIFICATION") + constants.DEFAULT_ROLE = os.Getenv("DEFAULT_ROLE") if constants.ADMIN_SECRET == "" { panic("root admin secret is required") @@ -143,4 +144,28 @@ func InitEnv() { constants.DISABLE_EMAIL_VERIFICATION = "false" } } + + rolesSplit := strings.Split(os.Getenv("ROLES"), ",") + roles := []string{} + defaultRole := "" + for _, val := range rolesSplit { + trimVal := strings.TrimSpace(val) + if trimVal != "" { + roles = append(roles, trimVal) + } + + if trimVal == constants.DEFAULT_ROLE { + defaultRole = trimVal + } + } + if len(roles) > 0 && defaultRole == "" { + panic(`Invalid DEFAULT_ROLE environment. It can be one from give ROLES environment variable value`) + } + + if len(roles) == 0 { + roles = []string{"user", "admin"} + constants.DEFAULT_ROLE = "user" + } + + constants.ROLES = roles } diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index beb3d50..a869075 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -97,6 +97,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int Image func(childComplexity int) int LastName func(childComplexity int) int + Roles func(childComplexity int) int SignupMethod func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -431,6 +432,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.LastName(childComplexity), true + case "User.roles": + if e.complexity.User.Roles == nil { + break + } + + return e.complexity.User.Roles(childComplexity), true + case "User.signupMethod": if e.complexity.User.SignupMethod == nil { break @@ -583,6 +591,7 @@ type User { image: String createdAt: Int64 updatedAt: Int64 + roles: [String] } type VerificationRequest { @@ -618,11 +627,13 @@ input SignUpInput { password: String! confirmPassword: String! image: String + roles: [String] } input LoginInput { email: String! password: String! + role: String } input VerifyEmailInput { @@ -641,6 +652,7 @@ input UpdateProfileInput { lastName: String image: String email: String + roles: [String] } input ForgotPasswordInput { @@ -2249,6 +2261,38 @@ func (ec *executionContext) _User_updatedAt(ctx context.Context, field graphql.C return ec.marshalOInt642ᚖint64(ctx, field.Selections, res) } +func (ec *executionContext) _User_roles(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "User", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Roles, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*string) + fc.Result = res + return ec.marshalOString2ᚕᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _VerificationRequest_id(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3625,6 +3669,14 @@ func (ec *executionContext) unmarshalInputLoginInput(ctx context.Context, obj in if err != nil { return it, err } + case "role": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) + it.Role, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } } } @@ -3741,6 +3793,14 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i if err != nil { return it, err } + case "roles": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles")) + it.Roles, err = ec.unmarshalOString2ᚕᚖstring(ctx, v) + if err != nil { + return it, err + } } } @@ -3809,6 +3869,14 @@ func (ec *executionContext) unmarshalInputUpdateProfileInput(ctx context.Context if err != nil { return it, err } + case "roles": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("roles")) + it.Roles, err = ec.unmarshalOString2ᚕᚖstring(ctx, v) + if err != nil { + return it, err + } } } @@ -4198,6 +4266,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._User_createdAt(ctx, field, obj) case "updatedAt": out.Values[i] = ec._User_updatedAt(ctx, field, obj) + case "roles": + out.Values[i] = ec._User_roles(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5002,6 +5072,42 @@ func (ec *executionContext) marshalOString2string(ctx context.Context, sel ast.S return graphql.MarshalString(v) } +func (ec *executionContext) unmarshalOString2ᚕᚖstring(ctx context.Context, v interface{}) ([]*string, error) { + if v == nil { + return nil, nil + } + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]*string, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalOString2ᚖstring(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOString2ᚕᚖstring(ctx context.Context, sel ast.SelectionSet, v []*string) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalOString2ᚖstring(ctx, sel, v[i]) + } + + return ret +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v interface{}) (*string, error) { if v == nil { return nil, nil diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index 1b564fe..ccf97a7 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -23,8 +23,9 @@ type ForgotPasswordInput struct { } type LoginInput struct { - Email string `json:"email"` - Password string `json:"password"` + Email string `json:"email"` + Password string `json:"password"` + Role *string `json:"role"` } type Meta struct { @@ -52,34 +53,37 @@ type Response struct { } type SignUpInput struct { - FirstName *string `json:"firstName"` - LastName *string `json:"lastName"` - Email string `json:"email"` - Password string `json:"password"` - ConfirmPassword string `json:"confirmPassword"` - Image *string `json:"image"` + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` + Email string `json:"email"` + Password string `json:"password"` + ConfirmPassword string `json:"confirmPassword"` + Image *string `json:"image"` + Roles []*string `json:"roles"` } type UpdateProfileInput struct { - OldPassword *string `json:"oldPassword"` - NewPassword *string `json:"newPassword"` - ConfirmNewPassword *string `json:"confirmNewPassword"` - FirstName *string `json:"firstName"` - LastName *string `json:"lastName"` - Image *string `json:"image"` - Email *string `json:"email"` + OldPassword *string `json:"oldPassword"` + NewPassword *string `json:"newPassword"` + ConfirmNewPassword *string `json:"confirmNewPassword"` + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` + Image *string `json:"image"` + Email *string `json:"email"` + Roles []*string `json:"roles"` } type User struct { - ID string `json:"id"` - Email string `json:"email"` - SignupMethod string `json:"signupMethod"` - FirstName *string `json:"firstName"` - LastName *string `json:"lastName"` - EmailVerifiedAt *int64 `json:"emailVerifiedAt"` - Image *string `json:"image"` - CreatedAt *int64 `json:"createdAt"` - UpdatedAt *int64 `json:"updatedAt"` + ID string `json:"id"` + Email string `json:"email"` + SignupMethod string `json:"signupMethod"` + FirstName *string `json:"firstName"` + LastName *string `json:"lastName"` + EmailVerifiedAt *int64 `json:"emailVerifiedAt"` + Image *string `json:"image"` + CreatedAt *int64 `json:"createdAt"` + UpdatedAt *int64 `json:"updatedAt"` + Roles []*string `json:"roles"` } type VerificationRequest struct { diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 04a14d3..ca9a448 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -23,6 +23,7 @@ type User { image: String createdAt: Int64 updatedAt: Int64 + roles: [String] } type VerificationRequest { @@ -58,11 +59,13 @@ input SignUpInput { password: String! confirmPassword: String! image: String + roles: [String] } input LoginInput { email: String! password: String! + role: String } input VerifyEmailInput { @@ -81,6 +84,7 @@ input UpdateProfileInput { lastName: String image: String email: String + roles: [String] } input ForgotPasswordInput { diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 50bb5cf..064387f 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -73,7 +73,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/server/main.go b/server/main.go index 58a3b4f..d189314 100644 --- a/server/main.go +++ b/server/main.go @@ -9,6 +9,7 @@ import ( "github.com/authorizerdev/authorizer/server/handlers" "github.com/authorizerdev/authorizer/server/oauth" "github.com/authorizerdev/authorizer/server/session" + "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-contrib/location" "github.com/gin-gonic/gin" ) @@ -50,6 +51,7 @@ func main() { db.InitDB() session.InitSession() oauth.InitOAuth() + utils.InitServer() r := gin.Default() r.Use(location.Default()) diff --git a/server/resolvers/signup.go b/server/resolvers/signup.go index 1c82515..c6ceb87 100644 --- a/server/resolvers/signup.go +++ b/server/resolvers/signup.go @@ -35,6 +35,15 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, return res, fmt.Errorf(`invalid email address`) } + if len(params.Roles) > 0 { + // check if roles exists + if !utils.IsValidRolesArray(params.Roles) { + return res, fmt.Errorf(`invalid roles`) + } + } else { + params.Roles = []*string{&constants.DEFAULT_ROLE} + } + // find user with email existingUser, err := db.Mgr.GetUserByEmail(params.Email) if err != nil { @@ -49,6 +58,13 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, Email: params.Email, } + roles := "" + for _, roleInput := range params.Roles { + roles += *roleInput + "," + } + roles = strings.TrimSuffix(roles, ",") + user.Roles = roles + password, _ := utils.HashPassword(params.Password) user.Password = password @@ -79,6 +95,7 @@ func Signup(ctx context.Context, params model.SignUpInput) (*model.AuthResponse, EmailVerifiedAt: &user.EmailVerifiedAt, CreatedAt: &user.CreatedAt, UpdatedAt: &user.UpdatedAt, + Roles: params.Roles, } if constants.DISABLE_EMAIL_VERIFICATION != "true" { diff --git a/server/utils/initServer.go b/server/utils/initServer.go new file mode 100644 index 0000000..a15c08d --- /dev/null +++ b/server/utils/initServer.go @@ -0,0 +1,25 @@ +package utils + +import ( + "log" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/db" +) + +// any jobs that we want to run at start of server can be executed here + +// 1. create roles table and add the roles list from env to table + +func InitServer() { + roles := []db.Role{} + for _, val := range constants.ROLES { + roles = append(roles, db.Role{ + Role: val, + }) + } + err := db.Mgr.SaveRoles(roles) + if err != nil { + log.Println(`Error saving roles`, err) + } +} diff --git a/server/utils/validateSuperAdmin.go b/server/utils/validateSuperAdmin.go deleted file mode 100644 index c19d62b..0000000 --- a/server/utils/validateSuperAdmin.go +++ /dev/null @@ -1,15 +0,0 @@ -package utils - -import ( - "github.com/authorizerdev/authorizer/server/constants" - "github.com/gin-gonic/gin" -) - -func IsSuperAdmin(gc *gin.Context) bool { - secret := gc.Request.Header.Get("x-authorizer-admin-secret") - if secret == "" { - return false - } - - return secret == constants.ADMIN_SECRET -} diff --git a/server/utils/validator.go b/server/utils/validator.go index b953045..05397bb 100644 --- a/server/utils/validator.go +++ b/server/utils/validator.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/authorizerdev/authorizer/server/constants" + "github.com/gin-gonic/gin" ) func IsValidEmail(email string) bool { @@ -29,3 +30,28 @@ func IsValidRedirectURL(url string) bool { return hasValidURL } + +func IsSuperAdmin(gc *gin.Context) bool { + secret := gc.Request.Header.Get("x-authorizer-admin-secret") + if secret == "" { + return false + } + + return secret == constants.ADMIN_SECRET +} + +func IsValidRolesArray(roles []*string) bool { + valid := true + currentRoleMap := map[string]bool{} + + for _, currentRole := range constants.ROLES { + currentRoleMap[currentRole] = true + } + for _, inputRole := range roles { + if !currentRoleMap[*inputRole] { + valid = false + break + } + } + return valid +}