From 03beceee8af8c46eae18b3abfd89095aef3798df Mon Sep 17 00:00:00 2001 From: Albin Antony Date: Wed, 4 Oct 2023 19:02:26 +0530 Subject: [PATCH] Add #193 Align APIs to GovStack: Change URL paths for onboard endpoints --- resources/config | 2 +- src/handlerv2/getorganizationbyid_handler.go | 27 ++++ src/handlerv2/getorganizationimage_handler.go | 28 ++++ src/handlerv2/gettoken_handler.go | 74 +++++++++ src/handlerv2/loginadminuser_handler.go | 87 +++++++++++ src/handlerv2/loginuser_handler.go | 69 ++++++++ src/handlerv2/updateorganization_handler.go | 64 ++++++++ .../updateorganizationcoverimage_handler.go | 55 +++++++ .../updateorganizationlogoimage_handler.go | 55 +++++++ src/handlerv2/validatephonenumber_handler.go | 88 +++++++++++ src/handlerv2/validateuseremail_handler.go | 66 ++++++++ src/handlerv2/verifyotp_handler.go | 72 +++++++++ src/handlerv2/verifyphonenumber_handler.go | 147 ++++++++++++++++++ src/httppathsv2/onboard_paths.go | 18 +++ src/httppathsv2/routes.go | 21 +++ 15 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 src/handlerv2/getorganizationbyid_handler.go create mode 100644 src/handlerv2/getorganizationimage_handler.go create mode 100644 src/handlerv2/gettoken_handler.go create mode 100644 src/handlerv2/loginadminuser_handler.go create mode 100644 src/handlerv2/loginuser_handler.go create mode 100644 src/handlerv2/updateorganization_handler.go create mode 100644 src/handlerv2/updateorganizationcoverimage_handler.go create mode 100644 src/handlerv2/updateorganizationlogoimage_handler.go create mode 100644 src/handlerv2/validatephonenumber_handler.go create mode 100644 src/handlerv2/validateuseremail_handler.go create mode 100644 src/handlerv2/verifyotp_handler.go create mode 100644 src/handlerv2/verifyphonenumber_handler.go diff --git a/resources/config b/resources/config index 3fea7ad..5a5f66a 160000 --- a/resources/config +++ b/resources/config @@ -1 +1 @@ -Subproject commit 3fea7ad570dae08e971db8a4abe3a14b6b7324d1 +Subproject commit 5a5f66a3fedf0660d85ae86cf902b9eac6a2f064 diff --git a/src/handlerv2/getorganizationbyid_handler.go b/src/handlerv2/getorganizationbyid_handler.go new file mode 100644 index 0000000..bf5641f --- /dev/null +++ b/src/handlerv2/getorganizationbyid_handler.go @@ -0,0 +1,27 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/org" +) + +// GetOrganizationByID Gets a single organization by given id +func GetOrganizationByID(w http.ResponseWriter, r *http.Request) { + organizationID := r.Header.Get(config.OrganizationId) + o, err := org.Get(organizationID) + + if err != nil { + m := fmt.Sprintf("Failed to get organization by ID :%v", organizationID) + common.HandleError(w, http.StatusNotFound, m, err) + return + } + + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + response, _ := json.Marshal(organization{o}) + w.Write(response) +} diff --git a/src/handlerv2/getorganizationimage_handler.go b/src/handlerv2/getorganizationimage_handler.go new file mode 100644 index 0000000..73cd3b6 --- /dev/null +++ b/src/handlerv2/getorganizationimage_handler.go @@ -0,0 +1,28 @@ +package handlerv2 + +import ( + "fmt" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/image" + "github.com/gorilla/mux" +) + +// GetOrganizationImage Retrieves the organization image +func GetOrganizationImage(w http.ResponseWriter, r *http.Request) { + organizationID := r.Header.Get(config.OrganizationId) + imageID := mux.Vars(r)["imageID"] + + image, err := image.Get(imageID) + + if err != nil { + m := fmt.Sprintf("Failed to fetch image with id: %v for org: %v", imageID, organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + w.Header().Set(config.ContentTypeHeader, config.ContentTypeImage) + w.Write(image.Data) +} diff --git a/src/handlerv2/gettoken_handler.go b/src/handlerv2/gettoken_handler.go new file mode 100644 index 0000000..861700b --- /dev/null +++ b/src/handlerv2/gettoken_handler.go @@ -0,0 +1,74 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" +) + +type tokenReq struct { + RefreshToken string `valid:"required"` + ClientID string `valid:"required"` +} + +// GetToken return access token when refresh token is given +func GetToken(w http.ResponseWriter, r *http.Request) { + var tReq tokenReq + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &tReq) + + // validating request payload for refreshing tokens + valid, err := govalidator.ValidateStruct(tReq) + + if !valid { + log.Printf("Failed to refresh token") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + data := url.Values{} + data.Set("refresh_token", tReq.RefreshToken) + data.Add("client_id", tReq.ClientID) + data.Add("grant_type", "refresh_token") + + resp, err := http.PostForm(iamConfig.URL+"/realms/"+iamConfig.Realm+"/protocol/openid-connect/token", data) + if err != nil { + //m := fmt.Sprintf("Failed to get token from refresh token for user:%v", token.GetUserName(r)) + m := fmt.Sprintf("Failed to get token from refresh token") + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + //m := fmt.Sprintf("Failed to get token from refresh token user:%v", token.GetUserName(r)) + m := fmt.Sprintf("Failed to get token from refresh token") + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if resp.StatusCode != http.StatusOK { + var e iamError + json.Unmarshal(body, &e) + response, _ := json.Marshal(e) + w.WriteHeader(resp.StatusCode) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(response) + return + } + + var tok iamToken + json.Unmarshal(body, &tok) + response, _ := json.Marshal(tok) + w.WriteHeader(resp.StatusCode) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(response) +} diff --git a/src/handlerv2/loginadminuser_handler.go b/src/handlerv2/loginadminuser_handler.go new file mode 100644 index 0000000..c602cc4 --- /dev/null +++ b/src/handlerv2/loginadminuser_handler.go @@ -0,0 +1,87 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/actionlog" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/token" + "github.com/bb-consent/api/src/user" +) + +type loginReq struct { + Username string `valid:"required,email"` + Password string `valid:"required"` +} + +type loginResp struct { + User user.User + Token iamToken +} + +// LoginAdminUser Implements the admin users login +func LoginAdminUser(w http.ResponseWriter, r *http.Request) { + var lReq loginReq + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + json.Unmarshal(b, &lReq) + + log.Printf("Login username: %v", lReq.Username) + + // validating the request payload + valid, err := govalidator.ValidateStruct(lReq) + + if !valid { + log.Printf("Invalid request params for authentication") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + t, status, iamErr, err := getToken(lReq.Username, lReq.Password, "igrant-ios-app", iamConfig.Realm) + if err != nil { + if (iamError{}) != iamErr { + resp, _ := json.Marshal(iamErr) + w.WriteHeader(status) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(resp) + return + } + m := fmt.Sprintf("Failed to get token for user:%v", lReq.Username) + common.HandleError(w, status, m, err) + return + } + accessToken, err := token.ParseToken(t.AccessToken) + if err != nil { + m := fmt.Sprintf("Failed to parse token for user:%v", lReq.Username) + common.HandleError(w, status, m, err) + return + } + + u, err := user.GetByIamID(accessToken.IamID) + if err != nil { + m := fmt.Sprintf("User: %v does not exist", lReq.Username) + common.HandleError(w, http.StatusUnauthorized, m, err) + return + } + + if len(u.Roles) == 0 { + //Normal user can not login with this API. + m := fmt.Sprintf("Non Admin User: %v tried admin login", lReq.Username) + common.HandleError(w, http.StatusForbidden, m, err) + return + } + + actionLog := fmt.Sprintf("%v logged in", u.Email) + actionlog.LogOrgSecurityCalls(u.ID.Hex(), u.Email, u.Roles[0].OrgID, actionLog) + lResp := loginResp{u, t} + resp, _ := json.Marshal(lResp) + w.WriteHeader(http.StatusOK) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(resp) +} diff --git a/src/handlerv2/loginuser_handler.go b/src/handlerv2/loginuser_handler.go new file mode 100644 index 0000000..448f98d --- /dev/null +++ b/src/handlerv2/loginuser_handler.go @@ -0,0 +1,69 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/user" +) + +// LoginUser Implements the user login +func LoginUser(w http.ResponseWriter, r *http.Request) { + var lReq loginReq + + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + json.Unmarshal(b, &lReq) + + log.Printf("Login username: %v", lReq.Username) + + // validating the request payload + valid, err := govalidator.ValidateStruct(lReq) + + if !valid { + log.Printf("Invalid request params for authentication") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + t, status, iamErr, err := getToken(lReq.Username, lReq.Password, "igrant-ios-app", iamConfig.Realm) + if err != nil { + if (iamError{}) != iamErr { + resp, _ := json.Marshal(iamErr) + w.WriteHeader(status) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(resp) + return + } + m := fmt.Sprintf("Failed to get token for user:%v", lReq.Username) + common.HandleError(w, status, m, err) + return + } + sanitizedUserName := common.Sanitize(lReq.Username) + + //TODO: Remove me when the auth server is per dev environment + u, err := user.GetByEmail(sanitizedUserName) + if err != nil { + m := fmt.Sprintf("Login failed for non existant user:%v", lReq.Username) + common.HandleError(w, http.StatusUnauthorized, m, err) + return + } + + if len(u.Roles) > 0 { + m := fmt.Sprintf("Login not allowed for admin users:%v", lReq.Username) + common.HandleError(w, http.StatusUnauthorized, m, err) + return + } + + resp, _ := json.Marshal(t) + w.WriteHeader(http.StatusOK) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.Write(resp) +} diff --git a/src/handlerv2/updateorganization_handler.go b/src/handlerv2/updateorganization_handler.go new file mode 100644 index 0000000..88bfa80 --- /dev/null +++ b/src/handlerv2/updateorganization_handler.go @@ -0,0 +1,64 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/org" + "github.com/bb-consent/api/src/user" +) + +type orgUpdateReq struct { + Name string + Location string + Description string + PolicyURL string +} + +// UpdateOrganization Updates an organization +func UpdateOrganization(w http.ResponseWriter, r *http.Request) { + var orgUpReq orgUpdateReq + b, _ := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + json.Unmarshal(b, &orgUpReq) + + organizationID := r.Header.Get(config.OrganizationId) + + o, err := org.Get(organizationID) + if err != nil { + m := fmt.Sprintf("Failed to get organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if strings.TrimSpace(orgUpReq.Name) != "" { + o.Name = orgUpReq.Name + } + if strings.TrimSpace(orgUpReq.Location) != "" { + o.Location = orgUpReq.Location + } + if strings.TrimSpace(orgUpReq.Description) != "" { + o.Description = orgUpReq.Description + } + if strings.TrimSpace(orgUpReq.PolicyURL) != "" { + o.PolicyURL = orgUpReq.PolicyURL + } + + orgResp, err := org.Update(o) + if err != nil { + m := fmt.Sprintf("Failed to update organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + go user.UpdateOrganizationsSubscribedUsers(orgResp) + //response, _ := json.Marshal(organization{orgResp}) + //w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusAccepted) + //w.Write(response) +} diff --git a/src/handlerv2/updateorganizationcoverimage_handler.go b/src/handlerv2/updateorganizationcoverimage_handler.go new file mode 100644 index 0000000..fe96b51 --- /dev/null +++ b/src/handlerv2/updateorganizationcoverimage_handler.go @@ -0,0 +1,55 @@ +package handlerv2 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/image" + "github.com/bb-consent/api/src/org" +) + +// UpdateOrganizationCoverImage Inserts the image and update the id to user +func UpdateOrganizationCoverImage(w http.ResponseWriter, r *http.Request) { + organizationID := r.Header.Get(config.OrganizationId) + + file, _, err := r.FormFile("orgimage") + if err != nil { + m := fmt.Sprintf("Failed to extract image organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + defer file.Close() + + buf := bytes.NewBuffer(nil) + _, err = io.Copy(buf, file) + if err != nil { + m := fmt.Sprintf("Failed to copy image organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + imageID, err := image.Add(buf.Bytes()) + if err != nil { + m := fmt.Sprintf("Failed to store image in data store organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + imageURL := "https://" + r.Host + "/v1/organizations/" + organizationID + "/image/" + imageID + o, err := org.UpdateCoverImage(organizationID, imageID, imageURL) + if err != nil { + m := fmt.Sprintf("Failed to update organization: %v with image: %v details", organizationID, imageID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + response, _ := json.Marshal(organization{o}) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/handlerv2/updateorganizationlogoimage_handler.go b/src/handlerv2/updateorganizationlogoimage_handler.go new file mode 100644 index 0000000..fea8c86 --- /dev/null +++ b/src/handlerv2/updateorganizationlogoimage_handler.go @@ -0,0 +1,55 @@ +package handlerv2 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/image" + "github.com/bb-consent/api/src/org" +) + +// UpdateOrganizationLogoImage Inserts the image and update the id to user +func UpdateOrganizationLogoImage(w http.ResponseWriter, r *http.Request) { + organizationID := r.Header.Get(config.OrganizationId) + + file, _, err := r.FormFile("orgimage") + if err != nil { + m := fmt.Sprintf("Failed to extract image organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + defer file.Close() + + buf := bytes.NewBuffer(nil) + _, err = io.Copy(buf, file) + if err != nil { + m := fmt.Sprintf("Failed to copy image organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + imageID, err := image.Add(buf.Bytes()) + if err != nil { + m := fmt.Sprintf("Failed to store image in data store organization: %v", organizationID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + imageURL := "https://" + r.Host + "/v1/organizations/" + organizationID + "/image/" + imageID + o, err := org.UpdateLogoImage(organizationID, imageID, imageURL) + if err != nil { + m := fmt.Sprintf("Failed to update organization: %v with image: %v details", organizationID, imageID) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + response, _ := json.Marshal(organization{o}) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/handlerv2/validatephonenumber_handler.go b/src/handlerv2/validatephonenumber_handler.go new file mode 100644 index 0000000..4e2eff4 --- /dev/null +++ b/src/handlerv2/validatephonenumber_handler.go @@ -0,0 +1,88 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "time" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/otp" + "github.com/bb-consent/api/src/user" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type validatePhoneNumberReq struct { + Phone string `valid:"required"` +} + +// ValidatePhoneNumber Check if the phone number is already in use +func ValidatePhoneNumber(w http.ResponseWriter, r *http.Request) { + var validateReq validatePhoneNumberReq + var valResp validateResp + + b, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(b, &validateReq) + + // validating request payload + valid, err := govalidator.ValidateStruct(validateReq) + if valid != true { + log.Printf("Missing mandatory params for validating phone number") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + valResp.Result = true + valResp.Message = "Phone number is not in use" + + sanitizedPhoneNumber := common.Sanitize(validateReq.Phone) + + //Check whether the phone number is unique + exist, err := user.PhoneNumberExist(sanitizedPhoneNumber) + if err != nil { + m := fmt.Sprintf("Failed to validate user phone number: %v", validateReq.Phone) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if exist == true { + valResp.Result = false + valResp.Message = "Phone number is in use" + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + return + } + + //Check whether the phone number is in otp colleciton + o, err := otp.PhoneNumberExist(sanitizedPhoneNumber) + if err != nil { + m := fmt.Sprintf("Failed to validate user phone number: %v", validateReq.Phone) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if o != (otp.Otp{}) { + if primitive.NewObjectID().Timestamp().Sub(o.ID.Timestamp()) > 2*time.Minute { + err = otp.Delete(o.ID.Hex()) + if err != nil { + m := fmt.Sprintf("Failed to clear expired otp") + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + } else { + valResp.Result = false + valResp.Message = "Phone number is in use" + } + } + + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/handlerv2/validateuseremail_handler.go b/src/handlerv2/validateuseremail_handler.go new file mode 100644 index 0000000..b94e5a6 --- /dev/null +++ b/src/handlerv2/validateuseremail_handler.go @@ -0,0 +1,66 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/user" +) + +type validateUserEmailReq struct { + Email string `valid:"required, email"` +} + +type validateResp struct { + Result bool //True for valid email + Message string +} + +// ValidateUserEmail Validates the user email +func ValidateUserEmail(w http.ResponseWriter, r *http.Request) { + var validateReq validateUserEmailReq + var valResp validateResp + + b, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(b, &validateReq) + + valid, err := govalidator.ValidateStruct(validateReq) + if valid != true { + valResp.Result = false + valResp.Message = err.Error() + + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + return + } + + valResp.Result = true + valResp.Message = "Email address is valid and not in use in our system" + + sanitizedEmail := common.Sanitize(validateReq.Email) + + //Check whether the email is unique + exist, err := user.EmailExist(sanitizedEmail) + if err != nil { + m := fmt.Sprintf("Failed to validate user email: %v", validateReq.Email) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + if exist == true { + valResp.Result = false + valResp.Message = "Email address is in use" + } + + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) +} diff --git a/src/handlerv2/verifyotp_handler.go b/src/handlerv2/verifyotp_handler.go new file mode 100644 index 0000000..944055b --- /dev/null +++ b/src/handlerv2/verifyotp_handler.go @@ -0,0 +1,72 @@ +package handlerv2 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/otp" +) + +type verifyOtpReq struct { + Phone string `valid:"required"` + Otp string `valid:"required"` +} + +// VerifyOtp Verifies the Otp +func VerifyOtp(w http.ResponseWriter, r *http.Request) { + var otpReq verifyOtpReq + var valResp validateResp + + b, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(b, &otpReq) + + valid, err := govalidator.ValidateStruct(otpReq) + if valid != true { + log.Printf("Missing mandatory params for verify otp") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + sanitizedPhoneNumber := common.Sanitize(otpReq.Phone) + + o, err := otp.SearchPhone(sanitizedPhoneNumber) + if err != nil { + valResp.Result = false + valResp.Message = "Unregistered phone number: " + otpReq.Phone + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + return + } + + valResp.Result = true + valResp.Message = "Otp validatiation Succeeded" + if err != nil || o.Otp != otpReq.Otp || o.Phone != otpReq.Phone { + valResp.Result = false + valResp.Message = "Otp validatiation failed with mismatch in otp data" + + } else { + o.Verified = true + //TODO: When user registration comes, locate the details and match and then remove this entry + //TODO: Periodic delete of stale OTP entries based on creation time needed + err := otp.UpdateVerified(o) + if err != nil { + m := fmt.Sprintf("Failed to update internal database") + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + } + + response, _ := json.Marshal(valResp) + w.Header().Set(config.ContentTypeHeader, config.ContentTypeJSON) + w.WriteHeader(http.StatusOK) + w.Write(response) + return +} diff --git a/src/handlerv2/verifyphonenumber_handler.go b/src/handlerv2/verifyphonenumber_handler.go new file mode 100644 index 0000000..835f052 --- /dev/null +++ b/src/handlerv2/verifyphonenumber_handler.go @@ -0,0 +1,147 @@ +package handlerv2 + +import ( + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + + "github.com/asaskevich/govalidator" + "github.com/bb-consent/api/src/common" + "github.com/bb-consent/api/src/config" + "github.com/bb-consent/api/src/otp" +) + +type verifyPhoneNumberReq struct { + Name string + Email string + Phone string `valid:"required"` +} + +// VerifyPhoneNumber Verifies the user phone number +func VerifyPhoneNumber(w http.ResponseWriter, r *http.Request) { + verifyPhoneNumber(w, r, common.ClientTypeIos) +} + +func generateVerificationCode() (code string, err error) { + var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} + codeSize := 6 + b := make([]byte, codeSize) + n, err := io.ReadAtLeast(rand.Reader, b, codeSize) + if n != codeSize { + return code, err + } + for i := 0; i < len(b); i++ { + b[i] = table[int(b[i])%len(table)] + } + return string(b), nil +} + +func sendPhoneVerificationMessage(msgTo string, name string, message string) error { + urlStr := "https://api.twilio.com/2010-04-01/Accounts/" + twilioConfig.AccountSid + "/Messages.json" + + // Pack up the data for our message + msgData := url.Values{} + + // Add "+" before the phone number + if !strings.Contains(msgTo, "+") { + msgTo = "+" + msgTo + } + + msgData.Set("To", msgTo) + + if strings.Contains(msgTo, "+1") { + msgData.Set("From", "+15063065105") + } else { + msgData.Set("From", "+46769437629") + } + msgData.Set("Body", message) + + msgDataReader := *strings.NewReader(msgData.Encode()) + + // Create HTTP request client + client := &http.Client{} + req, _ := http.NewRequest("POST", urlStr, &msgDataReader) + req.SetBasicAuth(twilioConfig.AccountSid, twilioConfig.AuthToken) + req.Header.Add("Accept", config.ContentTypeJSON) + req.Header.Add(config.ContentTypeHeader, config.ContentTypeFormURLEncoded) + + // Make HTTP POST request and return message SID + resp, _ := client.Do(req) + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + var data map[string]interface{} + decoder := json.NewDecoder(resp.Body) + defer resp.Body.Close() + err := decoder.Decode(&data) + if err == nil { + fmt.Println(data["sid"]) + } + } else { + fmt.Println(resp.Status) + return errors.New("Failed to send message") + } + return nil +} + +// verifyPhoneNumber Verifies the user phone number +func verifyPhoneNumber(w http.ResponseWriter, r *http.Request, clientType int) { + var verifyReq verifyPhoneNumberReq + + b, _ := ioutil.ReadAll(r.Body) + json.Unmarshal(b, &verifyReq) + + valid, err := govalidator.ValidateStruct(verifyReq) + if valid != true { + log.Printf("Invalid request params for verifying phone number") + common.HandleError(w, http.StatusBadRequest, err.Error(), err) + return + } + + vCode, err := generateVerificationCode() + if err != nil { + m := fmt.Sprintf("Failed to generate OTP :%v", verifyReq.Phone) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + + var message strings.Builder + message.Grow(32) + if clientType == common.ClientTypeAndroid { + fmt.Fprintf(&message, "[#]Thank you for signing up for iGrant.io! Your code is %s \n U1vUn/jAcoT", vCode) + } else { + fmt.Fprintf(&message, "Thank you for signing up for iGrant.io! Your code is %s", vCode) + } + + err = sendPhoneVerificationMessage(verifyReq.Phone, verifyReq.Name, message.String()) + if err != nil { + m := fmt.Sprintf("Failed to send sms to :%v", verifyReq.Phone) + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + var o otp.Otp + o.Name = verifyReq.Name + o.Email = verifyReq.Email + o.Phone = verifyReq.Phone + o.Otp = vCode + + sanitizedPhoneNumber := common.Sanitize(o.Phone) + + oldOtp, err := otp.SearchPhone(sanitizedPhoneNumber) + if err == nil { + otp.Delete(oldOtp.ID.Hex()) + } + + o, err = otp.Add(o) + if err != nil { + m := fmt.Sprintf("Failed to store otp details") + common.HandleError(w, http.StatusInternalServerError, m, err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/src/httppathsv2/onboard_paths.go b/src/httppathsv2/onboard_paths.go index 4d9bf1a..dcbaec7 100644 --- a/src/httppathsv2/onboard_paths.go +++ b/src/httppathsv2/onboard_paths.go @@ -1 +1,19 @@ package httppathsv2 + +// login +const LoginAdminUser = "/v2/onboard/admin/login" +const LoginUser = "/v2/onboard/individual/login" + +const ValidateUserEmail = "/v2/onboard/validate/email" +const ValidatePhoneNumber = "/v2/onboard/validate/phone" +const VerifyPhoneNumber = "/v2/onboard/verify/phone" +const VerifyOtp = "/v2/onboard/verify/otp" + +const GetToken = "/onboard/token" + +const GetOrganizationByID = "/v2/onboard/organisation" +const UpdateOrganization = "/v2/onboard/organisation" +const UpdateOrganizationCoverImage = "/v2/onboard/organisation/coverimage" +const UpdateOrganizationLogoImage = "/v2/onboard/organisation/logoimage" +const GetOrganizationCoverImage = "/v2/onboard/organisation/coverimage" +const GetOrganizationLogoImage = "/v2/onboard/organisation/logoimage" diff --git a/src/httppathsv2/routes.go b/src/httppathsv2/routes.go index 9f707ca..55799ba 100644 --- a/src/httppathsv2/routes.go +++ b/src/httppathsv2/routes.go @@ -1,6 +1,8 @@ package httppathsv2 import ( + "net/http" + handler "github.com/bb-consent/api/src/handlerv2" m "github.com/bb-consent/api/src/middleware" "github.com/casbin/casbin/v2" @@ -91,4 +93,23 @@ func SetRoutes(r *mux.Router, e *casbin.Enforcer) { // organization action logs r.Handle(GetOrgLogs, m.Chain(handler.GetOrgLogs, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("GET") + + // Onboard api(s) + + r.Handle(LoginAdminUser, m.Chain(handler.LoginAdminUser, m.LoggerNoAuth())).Methods("POST") + r.Handle(LoginUser, m.Chain(handler.LoginUser, m.LoggerNoAuth())).Methods("POST") + + r.Handle(ValidateUserEmail, m.Chain(handler.ValidateUserEmail, m.LoggerNoAuth())).Methods("POST") + r.Handle(ValidatePhoneNumber, m.Chain(handler.ValidatePhoneNumber, m.LoggerNoAuth())).Methods("POST") + r.Handle(VerifyPhoneNumber, m.Chain(handler.VerifyPhoneNumber, m.LoggerNoAuth())).Methods("POST") + r.Handle(VerifyOtp, m.Chain(handler.VerifyOtp, m.LoggerNoAuth())).Methods("POST") + + r.Handle(GetToken, http.HandlerFunc(handler.GetToken)).Methods("POST") + + r.Handle(GetOrganizationByID, m.Chain(handler.GetOrganizationByID, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("GET") + r.Handle(UpdateOrganization, m.Chain(handler.UpdateOrganization, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("PATCH") + r.Handle(UpdateOrganizationCoverImage, m.Chain(handler.UpdateOrganizationCoverImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("POST") + r.Handle(UpdateOrganizationLogoImage, m.Chain(handler.UpdateOrganizationLogoImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("POST") + r.Handle(GetOrganizationCoverImage, m.Chain(handler.GetOrganizationImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("GET") + r.Handle(GetOrganizationLogoImage, m.Chain(handler.GetOrganizationImage, m.Logger(), m.Authorize(e), m.SetApplicationMode(), m.Authenticate())).Methods("GET") }