diff --git a/application/applicationservice/user.go b/application/applicationservice/user.go index 1772486..b191e31 100644 --- a/application/applicationservice/user.go +++ b/application/applicationservice/user.go @@ -149,3 +149,23 @@ func (s *userService) GetContactType() (retVal []viewmodel.ContactType, err erro return s.mapEntity.User.ToContactTypeModelSlice(entity), nil } + +func (s *userService) SaveContact(contact viewmodel.Contact) (retVal viewmodel.Contact, err error) { + entity := s.mapEntity.User.ToContactEntity(contact) + entity, err = s.svc.Users.SaveContact(entity) + if err != nil { + return retVal, errors.Wrap(err) + } + + return s.mapEntity.User.ToContactModel(entity), err +} + +func (s *userService) RemoveContact(contact viewmodel.Contact) (retVal viewmodel.Contact, err error) { + entity := s.mapEntity.User.ToContactEntity(contact) + entity, err = s.svc.Users.RemoveContact(entity) + if err != nil { + return retVal, errors.Wrap(err) + } + + return s.mapEntity.User.ToContactModel(entity), err +} diff --git a/application/entitymapping/user.go b/application/entitymapping/user.go index a2c3473..baee854 100644 --- a/application/entitymapping/user.go +++ b/application/entitymapping/user.go @@ -158,8 +158,11 @@ func (mapping *userMapping) ToContactTypeModelSlice(list []entity.ContactType) ( // ToContactModel maps a Contact entity to Contact view model func (mapping *userMapping) ToContactModel(item entity.ContactInfo) viewmodel.Contact { return viewmodel.Contact{ - Type: mapping.ToContactTypeModel(item.Type), - Value: item.Value, + ID: item.UUID, + User: mapping.ToUserModel(item.User), + Author: mapping.ToUserModel(item.Author), + Type: mapping.ToContactTypeModel(item.Type), + Value: item.Value, } } @@ -177,8 +180,11 @@ func (mapping *userMapping) ToContactModelSlice(list []entity.ContactInfo) (retV // ToContactEntity maps a Contact entity to Contact view model func (mapping *userMapping) ToContactEntity(item viewmodel.Contact) entity.ContactInfo { return entity.ContactInfo{ - Type: mapping.ToContactTypeEntity(item.Type), - Value: item.Value, + UUID: item.ID, + User: mapping.ToUserEntity(item.User), + Author: mapping.ToUserEntity(item.Author), + Type: mapping.ToContactTypeEntity(item.Type), + Value: item.Value, } } diff --git a/application/viewmodel/tnc.go b/application/viewmodel/tnc.go index e5cefbf..380e41e 100644 --- a/application/viewmodel/tnc.go +++ b/application/viewmodel/tnc.go @@ -15,6 +15,7 @@ type WebhookResponse struct { //RideRequest has the data to dispatch a ride type RideRequest struct { UserUUID string `json:"user_uuid,omitempty"` + UserConsent bool `json:"user_consent,omitempty"` Status string `json:"status,omitempty"` RideID string `json:"ride_id,omitempty"` RideType string `json:"ride_type,omitempty"` @@ -158,4 +159,4 @@ type RideRoute struct { Duration int64 `json:"duration,omitempty"` ETA int64 `json:"eta,omitempty"` Bearing int64 `json:"bearing,omitempty"` -} +} \ No newline at end of file diff --git a/application/viewmodel/user.go b/application/viewmodel/user.go index 1242485..abe337c 100644 --- a/application/viewmodel/user.go +++ b/application/viewmodel/user.go @@ -26,8 +26,11 @@ type User struct { } type Contact struct { - Type ContactType `json:"type,omitempty"` - Value string `json:"contact,omitempty"` + ID string `json:"id,omitempty"` + User User `json:"-"` + Type ContactType `json:"type,omitempty"` + Value string `json:"contact,omitempty"` + Author User `json:"-"` } type ContactType struct { diff --git a/authorization_policy.csv b/authorization_policy.csv index 107e45f..0f3e882 100644 --- a/authorization_policy.csv +++ b/authorization_policy.csv @@ -79,18 +79,23 @@ p, *, *, *, *, *, *, /v1/nemt/organization/type, GET p, AD, *, *, *, *, *, /v1/nemt/organization/*, GET p, AD, *, *, *, *, *, /v1/nemt/organization/*, POST p, AD, *, *, *, *, *, /v1/nemt/organization/*, PUT +p, AD, *, *, *, *, *, /v1/nemt/organization/*, DELETE p, SCHDAD, *, *, *, [equal*], *, /v1/nemt/organization/*, GET p, SCHDAD, *, *, *, [equal*], *, /v1/nemt/organization/*, POST p, SCHDAD, *, *, *, [equal*], *, /v1/nemt/organization/*, PUT +p, SCHDAD, *, *, *, [equal*], *, /v1/nemt/organization/*, DELETE p, PLANAD, *, *, *, [equal*], *, /v1/nemt/organization/*, GET p, PLANAD, *, *, *, [equal*], *, /v1/nemt/organization/*, POST p, PLANAD, *, *, *, [equal*], *, /v1/nemt/organization/*, PUT +p, PLANAD, *, *, *, [equal*], *, /v1/nemt/organization/*, DELETE p, BDCAD, *, *, *, *, *, /v1/nemt/organization/*, GET p, BDCAD, *, *, *, *, *, /v1/nemt/organization/*, POST p, BDCAD, *, *, *, *, *, /v1/nemt/organization/*, PUT +p, BDCAD, *, *, *, *, *, /v1/nemt/organization/*, DELETE p, BCBSIAD, *, *, *, *, *, /v1/nemt/organization/*, GET p, BCBSIAD, *, *, *, *, *, /v1/nemt/organization/*, POST p, BCBSIAD, *, *, *, *, *, /v1/nemt/organization/*, PUT +p, BCBSIAD, *, *, *, *, *, /v1/nemt/organization/*, DELETE p, SPT, *, programsupport, *, *, *, /v1/nemt/organization/*, GET p, SP, *, provider, *, *, *, /v1/nemt/organization, GET p, SP, *, plan, *, *, *, /v1/nemt/organization, GET @@ -108,4 +113,3 @@ p, BCBSIAD, *, bcbsi, *, *, *, /v1/nemt/eligibility, POST p, BDCAD, *, techsupport, *, *, *, /v1/nemt/eligibility, POST p, PLANAD, *, plan, *, *, *, /v1/nemt/eligibility, POST p, AD, *, *, *, *, *, /v1/nemt/eligibility, POST - diff --git a/data/datamysql/organization.go b/data/datamysql/organization.go index 41d9bf6..3673bb4 100644 --- a/data/datamysql/organization.go +++ b/data/datamysql/organization.go @@ -294,7 +294,7 @@ func (c *organizationRepo) GetAllTypes() ([]entity.OrganizationType, error) { } func (c *organizationRepo) GetTypeByKey(key string) (entity.OrganizationType, error) { - return c.parseTypeEntity(c.conn.QueryRow(c.getTypeQuery()+" WHERE b.organization_type_key=?", key)) + return c.parseTypeEntity(c.conn.QueryRow(c.getTypeQuery()+" WHERE organization_type_key=? ", key)) } func (c *organizationRepo) GetByType(organizationTypeKey string, user entity.User) ([]entity.Organization, error) { diff --git a/data/datamysql/user.go b/data/datamysql/user.go index 2ae8be0..1ece11f 100644 --- a/data/datamysql/user.go +++ b/data/datamysql/user.go @@ -63,7 +63,8 @@ func (c *userRepo) GetContacts(userID int64) ([]entity.ContactInfo, error) { a.user_id, b.contact_type_id, b.key contact_type_key, - b.name contact_type_name + b.name contact_type_name, + a.contact_uuid FROM tab_contact a INNER JOIN tab_contact_type b ON a.contact_type_id = b.contact_type_id @@ -79,7 +80,7 @@ func (c *userRepo) GetContacts(userID int64) ([]entity.ContactInfo, error) { retVal := make([]entity.ContactInfo, 0) for rows.Next() { contact := entity.ContactInfo{} - err = rows.Scan(&contact.ID, &contact.Value, &contact.UserID, &contact.Type.ID, &contact.Type.Key, &contact.Type.Value) + err = rows.Scan(&contact.ID, &contact.Value, &contact.User.ID, &contact.Type.ID, &contact.Type.Key, &contact.Type.Value, &contact.UUID) if err != nil { return nil, err } @@ -354,9 +355,11 @@ func (c *userRepo) Create(user entity.User) (retVal entity.User, err error) { if retVal.Email != "" { contact := entity.ContactInfo{ - Type: entity.ContactType{Key: "email"}, - Value: retVal.Email, - UserID: retVal.ID, + Type: entity.ContactType{Key: "email"}, + Value: retVal.Email, + User: entity.User{ + ID: retVal.ID, + }, } contact, err = c.addContactInfo(contact) @@ -367,9 +370,11 @@ func (c *userRepo) Create(user entity.User) (retVal entity.User, err error) { if retVal.PhoneNumber != "" { contact := entity.ContactInfo{ - Type: entity.ContactType{Key: "phone"}, - Value: retVal.PhoneNumber, - UserID: retVal.ID, + Type: entity.ContactType{Key: "phone"}, + Value: retVal.PhoneNumber, + User: entity.User{ + ID: retVal.ID, + }, } contact, err = c.addContactInfo(contact) @@ -387,19 +392,10 @@ func (c *userRepo) SaveContact(contact entity.ContactInfo) (entity.ContactInfo, func (c *userRepo) RemoveContact(contact entity.ContactInfo) (entity.ContactInfo, error) { const ( - query = `INSERT INTO tab_contact(contact_type_id, user_id, contact) - SELECT a.contact_type_id, ? user_id, ? contact - FROM - tab_contact_type a - LEFT JOIN tab_contact b - ON a.contact_type_id = b.contact_type_id - AND b.user_id = ? - AND b.contact = ? - WHERE a.key = ? - AND b.contact_id IS NULL;` + query = `DELETE FROM tab_contact WHERE contact_uuid = ?;` ) - result, err := c.conn.Exec(query, contact.UserID, contact.Value, contact.UserID, contact.Value, contact.Type.Key) + result, err := c.conn.Exec(query, contact.UUID) if err != nil { return contact, err } @@ -415,8 +411,8 @@ func (c *userRepo) RemoveContact(contact entity.ContactInfo) (entity.ContactInfo func (c *userRepo) addContactInfo(contact entity.ContactInfo) (entity.ContactInfo, error) { const ( - query = `INSERT INTO tab_contact(contact_type_id, user_id, contact) - SELECT a.contact_type_id, ? user_id, ? contact + query = `INSERT INTO tab_contact(contact_type_id, user_id, contact, contact_uuid) + SELECT a.contact_type_id, ? user_id, ? contact, ? contact_uuid FROM tab_contact_type a LEFT JOIN tab_contact b @@ -427,7 +423,13 @@ func (c *userRepo) addContactInfo(contact entity.ContactInfo) (entity.ContactInf AND b.contact_id IS NULL;` ) - result, err := c.conn.Exec(query, contact.UserID, contact.Value, contact.UserID, contact.Value, contact.Type.Key) + sUUID, err := uuid.NewV4() + if err != nil { + return contact, err + } + + contact.UUID = sUUID.String() + result, err := c.conn.Exec(query, contact.User.ID, contact.Value, contact.UUID, contact.User.ID, contact.Value, contact.Type.Key) if err != nil { return contact, err } diff --git a/domain/contract/repo.go b/domain/contract/repo.go index d1775b8..f8940d0 100644 --- a/domain/contract/repo.go +++ b/domain/contract/repo.go @@ -27,6 +27,8 @@ type UserRepo interface { GetAddressByUUID(addressUUID string) (entity.Address, error) GetContactType() (retVal []entity.ContactType, err error) RemoveAddress(addressUUID string) error + SaveContact(contact entity.ContactInfo) (entity.ContactInfo, error) + RemoveContact(contact entity.ContactInfo) (entity.ContactInfo, error) } // RideRepo defines the data set for Rides diff --git a/domain/entity/user.go b/domain/entity/user.go index 1a95f1e..ba85a7e 100644 --- a/domain/entity/user.go +++ b/domain/entity/user.go @@ -34,8 +34,10 @@ type User struct { type ContactInfo struct { ID int64 `db:"contact_id" json:"contact_id"` + UUID string `db:"contact_uuid" json:"contact_uuid"` Type ContactType `db:"contact_type" json:"contact_type"` - UserID int64 `db:"user_id" json:"-"` + User User `db:"user" json:"-"` + Author User `db:"author" json:"-"` Value string `db:"value" json:"value"` } diff --git a/domain/service/user.go b/domain/service/user.go index 844c979..d5e31a3 100644 --- a/domain/service/user.go +++ b/domain/service/user.go @@ -82,6 +82,26 @@ func (s *userService) SaveAddress(address entity.Address) (entity.Address, error return s.svc.db.Users().SaveAddress(address) } +func (s *userService) SaveContact(contact entity.ContactInfo) (entity.ContactInfo, error) { + user, err := s.svc.db.Users().GetByUUID(contact.User.UUID, "") + if err != nil { + return entity.ContactInfo{}, nil + } + + contact.User = user + return s.svc.db.Users().SaveContact(contact) +} + +func (s *userService) RemoveContact(contact entity.ContactInfo) (entity.ContactInfo, error) { + user, err := s.svc.db.Users().GetByUUID(contact.User.UUID, "") + if err != nil { + return entity.ContactInfo{}, nil + } + + contact.User = user + return s.svc.db.Users().RemoveContact(contact) +} + // GetAddressByUUID returns a list of users by profile func (s *userService) GetAddressByUUID(addressUUID string) (entity.Address, error) { return s.svc.db.Users().GetAddressByUUID(addressUUID) diff --git a/server/authorization/address.go b/server/authorization/address.go new file mode 100644 index 0000000..467fcc9 --- /dev/null +++ b/server/authorization/address.go @@ -0,0 +1,15 @@ +package authorization + +import ( + + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" +) + +func CanCreateAddress(user viewmodel.User, organization viewmodel.Organization) bool { + //rules are the same for address creation and for organization creation + return CanCreateOrganization(user, organization) +} + +func CanUpdateAddress(user viewmodel.User, organization viewmodel.Organization) bool { + return CanCreateAddress(user, organization) +} \ No newline at end of file diff --git a/server/authorization/authorization.go b/server/authorization/authorization.go new file mode 100644 index 0000000..9f58f24 --- /dev/null +++ b/server/authorization/authorization.go @@ -0,0 +1 @@ +package authorization diff --git a/server/authorization/contact.go b/server/authorization/contact.go new file mode 100644 index 0000000..97612f9 --- /dev/null +++ b/server/authorization/contact.go @@ -0,0 +1,15 @@ +package authorization + +import ( + + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" +) + +func CanCreateContact(user viewmodel.User, organization viewmodel.Organization) bool { + //rules are the same for contact creation and for organization creation + return CanCreateOrganization(user, organization) +} + +func CanUpdateContact(user viewmodel.User, organization viewmodel.Organization) bool { + return CanCreateAddress(user, organization) +} \ No newline at end of file diff --git a/server/authorization/organization.go b/server/authorization/organization.go new file mode 100644 index 0000000..07b0936 --- /dev/null +++ b/server/authorization/organization.go @@ -0,0 +1,68 @@ +package authorization + +import ( + "fmt" + + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" +) + +func isAChildOrganization(potentialParent viewmodel.Organization, potentialChild viewmodel.Organization) bool { + for _, org := range potentialParent.ChildOrgs { + if potentialChild.UUID == org.UUID { + return true + } + } + return false +} + +func isSameOrganization(organizationA viewmodel.Organization, organizationB viewmodel.Organization) bool { + return organizationA.UUID == organizationB.UUID +} + +func grabOrgFromUser(user viewmodel.User) (viewmodel.Organization, error) { + if len(user.Organizations) < 1 { + return viewmodel.Organization{}, fmt.Errorf("User has no organizations %v", user) + } + + return user.Organizations[0], nil +} + +func CanCreateOrganization(user viewmodel.User, organization viewmodel.Organization ) bool { + userRole, err := grabProfileFromUser(user) + if err != nil { + return false + } + + /* + Admin BCBSI + Admin Technical Support + Super Admin Technical Support + + Manage all Organizations*/ + if userRole.Key == bcbsiAdmin || userRole.Key == brighterDevAdmin || userRole.Key == superAdmin{ + return true + } + + userOrg, err := grabOrgFromUser(user) + if err != nil{ + return false + } + + /* + Admin Provider + Admin Plan + + Manage the authenticated Authorized User's Organization and child Organizations */ + if userRole.Key == providerAdmin || userRole.Key == planAdmin{ + if isSameOrganization(userOrg, organization) || isAChildOrganization(userOrg, organization) { + return true + } + return false + } + + return false +} + +func CanUpdateOrganization(user viewmodel.User, organization viewmodel.Organization) bool{ + return CanCreateOrganization(user, organization) +} \ No newline at end of file diff --git a/server/authorization/profile.go b/server/authorization/profile.go new file mode 100644 index 0000000..1a43031 --- /dev/null +++ b/server/authorization/profile.go @@ -0,0 +1,62 @@ +package authorization + +import ( + "fmt" + + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" +) + +const ( + superAdmin = "AD" + scheduler = "SP" + support = "SPT" + member = "US" + brighterDevAdmin = "BDCAD" + bcbsiAdmin = "BCBSIAD" + planAdmin = "PLANAD" + providerAdmin = "SCHDAD" +) + +func grabProfileFromUser(user viewmodel.User) (viewmodel.Profile, error) { + if len(user.Profiles) < 1 { + return viewmodel.Profile{}, fmt.Errorf("User has no profiles %v", user) + } + return user.Profiles[0], nil +} + +func morePrivileged(who viewmodel.Profile, towardsWhom viewmodel.Profile) bool { + order := []string{superAdmin, brighterDevAdmin, bcbsiAdmin, planAdmin, providerAdmin, support, scheduler, member} + for _, value := range order { + if value == who.Key { + return true + } + + if value == towardsWhom.Key { + return false + } + } + // should hapen only in case profile key is empty + // and that's something fishy so let's deny! + return false +} + +func equallyOrMorePrivileged(who viewmodel.Profile, towardsWhom viewmodel.Profile) bool { + if who.Key == towardsWhom.Key { + return true + } + + return morePrivileged(who, towardsWhom) + +} + +func lessPrivilegedThanAdmin(who viewmodel.Profile) bool { + switch who.Key { + case member: + return true + case scheduler: + return true + case support: + return true + } + return false +} diff --git a/server/authorization/user.go b/server/authorization/user.go new file mode 100644 index 0000000..6e23a4c --- /dev/null +++ b/server/authorization/user.go @@ -0,0 +1,86 @@ +package authorization + +import "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" + +/* +CanCreateUser returns true if currentUser is allowed to create updatingUser according to +authorization rules +*/ +func CanCreateUser(currentUser viewmodel.User, updatingUser viewmodel.User) bool { + + if len(currentUser.Profiles) < 1 { + return false + } + + if len(updatingUser.Profiles) < 1 { + return false + } + + currentUserOrganization, err := grabOrgFromUser(currentUser) + if err != nil { + return false + } + + updatingUserOrganization, err := grabOrgFromUser(updatingUser) + if err != nil { + return false + } + + currentUserRole, err := grabProfileFromUser(currentUser) + if err != nil { + return false + } + + updatingUserRole, err := grabProfileFromUser(updatingUser) + if err != nil { + return false + } + + /* + Admin Provider + Manage all Authorized Users of the provider Organization or child organization + + The (Provider) Admin can manage Authorized Users of their Parent/ Top-level Org , but not Admins + */ + + currentUserHigherOrEqualOrg := isSameOrganization(currentUserOrganization, updatingUserOrganization) || isAChildOrganization(currentUserOrganization, updatingUserOrganization) + currentUserLowerOrg := isAChildOrganization(updatingUserOrganization, currentUserOrganization) + if currentUserRole.Key == providerAdmin && currentUserHigherOrEqualOrg && equallyOrMorePrivileged(currentUserRole, updatingUserRole) { + return true + } + if currentUserRole.Key == providerAdmin && currentUserLowerOrg && lessPrivilegedThanAdmin(updatingUserRole) { + return true + } + + /* Admin BCBSI + Manage all Authorized Users except Admins + + return false + */ + + if currentUserRole.Key == bcbsiAdmin && lessPrivilegedThanAdmin(updatingUserRole) { + return true + } + + /* Admin Technical Support Manage all Authorized Users except Admins */ + + if currentUserRole.Key == brighterDevAdmin && lessPrivilegedThanAdmin(updatingUserRole) { + return true + } + + /* Admin Plan Manage all Authorized Users of a single participating Plan except Admins */ + + if currentUserRole.Key == planAdmin && lessPrivilegedThanAdmin(updatingUserRole) && isSameOrganization(currentUserOrganization, updatingUserOrganization) { + return true + } + + /* Super Admin Technical Support + Manage all Members, INCLUDING Admins */ + + if currentUserRole.Key == superAdmin { + return true + } + + return false + +} diff --git a/server/router/organizationroute/controller.go b/server/router/organizationroute/controller.go index 682a896..1044379 100644 --- a/server/router/organizationroute/controller.go +++ b/server/router/organizationroute/controller.go @@ -10,6 +10,7 @@ import ( "bitbucket.org/nemt/nemt-portal-api/infra/cache" "bitbucket.org/nemt/nemt-portal-api/infra/config" "bitbucket.org/nemt/nemt-portal-api/server/router/routeutils" + "bitbucket.org/nemt/nemt-portal-api/server/authorization" "github.com/labstack/echo" ) @@ -64,6 +65,11 @@ func (c *controller) handleAddOrganization(ctx echo.Context) error { if err != nil { return routeutils.HandleAPIError(ctx, err) } + + if !authorization.CanCreateOrganization(authUser, org) { + return routeutils.ResponseAPIAuthorizationError(ctx) + } + org.Author.ID = authUser.ID org.LastEditor.ID = authUser.ID @@ -127,6 +133,15 @@ func (c *controller) handleParent(ctx echo.Context) error { return routeutils.HandleAPIError(ctx, err) } + organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + if !authorization.CanUpdateOrganization(authUser, organization){ + return routeutils.ResponseAPIAuthorizationError(ctx) + } + resp, err := c.svc.Organization.SetParentOrganization(orgUUID, parent.UUID, authUser) if err != nil { return routeutils.HandleAPIError(ctx, err) @@ -152,6 +167,15 @@ func (c *controller) handleChild(ctx echo.Context) error { return routeutils.HandleAPIError(ctx, err) } + organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + if !authorization.CanUpdateOrganization(authUser, organization){ + return routeutils.ResponseAPIAuthorizationError(ctx) + } + _, err = c.svc.Organization.SetParentOrganization(child.UUID, orgUUID, authUser) if err != nil { return routeutils.HandleAPIError(ctx, err) @@ -246,6 +270,18 @@ func (c *controller) handleAddAddress(ctx echo.Context) error { if err != nil { return routeutils.HandleAPIError(ctx, err) } + + organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + if !authorization.CanCreateAddress(authUser, organization) { + return routeutils.ResponseAPIAuthorizationError(ctx) + } + + return routeutils.ResponseAPIAuthorizationError(ctx) + address.CreatedUser.ID = authUser.ID address.UpdatedUser.ID = authUser.ID @@ -278,6 +314,7 @@ func (c *controller) handleRemoveContact(ctx echo.Context) error { if err != nil { return routeutils.HandleAPIError(ctx, err) } + contact.UpdatedUser.ID = authUser.ID err = c.svc.Organization.InactivateOrganizationContact(orgUUID, contact, authUser) @@ -309,6 +346,16 @@ func (c *controller) handleAddContact(ctx echo.Context) error { if err != nil { return routeutils.HandleAPIError(ctx, err) } + + organization, err := c.svc.Organization.GetByUUID(orgUUID, authUser) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + if !authorization.CanCreateContact(authUser, organization) { + return routeutils.ResponseAPIAuthorizationError(ctx) + } + contact.CreatedUser.ID = authUser.ID contact.UpdatedUser.ID = authUser.ID diff --git a/server/router/routeutils/response.go b/server/router/routeutils/response.go index 0d95b6f..f4981ac 100644 --- a/server/router/routeutils/response.go +++ b/server/router/routeutils/response.go @@ -33,6 +33,18 @@ func ResponseAPIOK(c echo.Context, data interface{}) error { return c.JSON(http.StatusOK, data) } +// ResponseAPIErrorWithData returns a standard API error with additional data to the response +func ResponseAPIErrorWithData(c echo.Context, status int, message string, redirect bool, data interface{}) error { + returnValue := resultWrapper{ + Error: true, + Message: message, + Redirect: redirect, + Data: data, + } + + return c.JSON(status, returnValue) +} + // ResponseAPIError returns a standard API error to the response func ResponseAPIError(c echo.Context, status int, message string, redirect bool) error { returnValue := resultWrapper{ @@ -49,6 +61,11 @@ func ResponseAPIAuthError(c echo.Context, message string, redirect bool) error { return ResponseAPIError(c, http.StatusUnauthorized, message, redirect) } +// ResponseAPIAuthorizationError returns a standard API auth error to the response +func ResponseAPIAuthorizationError(c echo.Context) error { + return ResponseAPIError(c, http.StatusForbidden, "Forbidden by controller", false) +} + // ResponseAPIServiceError returns a standard API service unavailable error to the response func ResponseAPIServiceError(c echo.Context, message string) error { return ResponseAPIError(c, http.StatusServiceUnavailable, message, false) @@ -59,6 +76,11 @@ func ResponseAPIValidationError(c echo.Context, message string) error { return ResponseAPIError(c, http.StatusUnprocessableEntity, message, false) } +// ResponseAPICustomValidationError returns a standard API validation error with custom data to the response +func ResponseAPICustomValidationError(c echo.Context, message string, data interface{}) error { + return ResponseAPIErrorWithData(c, http.StatusUnprocessableEntity, message, false, data) +} + // ResponseAPIFieldValidationError returns a standard API field validation error to the response func ResponseAPIFieldValidationError(c echo.Context, field string, message string) error { err := errors.NewValidationError(field, message) diff --git a/server/router/tncroute/controller.go b/server/router/tncroute/controller.go index 6e11d24..736b49d 100644 --- a/server/router/tncroute/controller.go +++ b/server/router/tncroute/controller.go @@ -15,6 +15,7 @@ import ( "bitbucket.org/nemt/nemt-portal-api/infra/auth" "bitbucket.org/nemt/nemt-portal-api/infra/config" "bitbucket.org/nemt/nemt-portal-api/server/router/routeutils" + "bitbucket.org/nemt/nemt-portal-api/server/validation" "github.com/labstack/echo" uuid "github.com/satori/go.uuid" "google.golang.org/api/googleapi/transport" @@ -231,6 +232,11 @@ func (c *controller) handle(ctx echo.Context) error { return routeutils.ResponseAPIValidationError(ctx, "User not found") } + //Validate ride request + if validationErrors := validation.ValidateRide(&requestRide, &user) ; len(validationErrors) > 0 { + return routeutils.ResponseAPICustomValidationError(ctx, "ride validation failed", validationErrors) + } + createdUser, err := auth.GetUserDetail(ctx, c.cfg) if err != nil { return routeutils.HandleAPIError(ctx, err) @@ -869,4 +875,4 @@ func (c *controller) handleReady(ctx echo.Context) error { }() return routeutils.ResponseAPIOK(ctx, nextRide) -} +} \ No newline at end of file diff --git a/server/router/usersroute/controller.go b/server/router/usersroute/controller.go index 34ff679..35543a9 100644 --- a/server/router/usersroute/controller.go +++ b/server/router/usersroute/controller.go @@ -13,6 +13,7 @@ import ( "bitbucket.org/nemt/nemt-portal-api/infra/auth" "bitbucket.org/nemt/nemt-portal-api/infra/cache" "bitbucket.org/nemt/nemt-portal-api/infra/config" + "bitbucket.org/nemt/nemt-portal-api/server/authorization" "bitbucket.org/nemt/nemt-portal-api/server/router/routeutils" "github.com/labstack/echo" ) @@ -127,6 +128,62 @@ func (c *controller) handleRemoveAddress(ctx echo.Context) error { return routeutils.ResponseNoContent(ctx, addressID) } +func (c *controller) handlePortalContact(ctx echo.Context) error { + userID, err := routeutils.GetAndValidateStringParam(ctx, "user_uuid", "mandatory field") + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + item, err := c.svc.Users.GetByUUID(userID, "") + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + createdUser, err := auth.GetUserDetail(ctx, c.cfg) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + if item.ID == "" { + return routeutils.ResponseAPIValidationError(ctx, "User not found") + } else { + var Contact viewmodel.Contact + if err := ctx.Bind(&Contact); err != nil { + return routeutils.HandleAPIError(ctx, err) + } + Contact.User = item + Contact.Author = createdUser + Contact, err = c.svc.Users.SaveContact(Contact) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + // Contact.User, err = c.svc.Users.GetByUUID(userID, "") + // if err != nil { + // return routeutils.HandleAPIError(ctx, err) + // } + + return routeutils.ResponseAPIOK(ctx, Contact) + } +} + +func (c *controller) handleRemoveContact(ctx echo.Context) error { + contactUUID, err := routeutils.GetAndValidateStringParam(ctx, "contact_uuid", "mandatory field") + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + contact := viewmodel.Contact{ + ID: contactUUID, + } + contact, err = c.svc.Users.RemoveContact(contact) + if err != nil { + return routeutils.HandleAPIError(ctx, err) + } + + return routeutils.ResponseNoContent(ctx, contact) +} + func (c *controller) handleMemberAddress(ctx echo.Context) error { userID, err := routeutils.GetAndValidateStringParam(ctx, "user_uuid", "mandatory field") if err != nil { @@ -389,6 +446,10 @@ func (c *controller) handlePortal(ctx echo.Context) error { return routeutils.HandleAPIError(ctx, err) } + if !authorization.CanCreateUser(authUser, user) { + return routeutils.ResponseAPIAuthorizationError(ctx) + } + if len(user.Profiles) == 0 { return routeutils.ResponseAPIAuthError(ctx, "profile is required", false) } diff --git a/server/router/usersroute/router.go b/server/router/usersroute/router.go index 57fe11c..34a7952 100644 --- a/server/router/usersroute/router.go +++ b/server/router/usersroute/router.go @@ -17,6 +17,8 @@ const ( userDetailRoute = "/portal/:user_uuid" userAddressRoute = "/portal/:user_uuid/address" userRemoveAddressRoute = "/portal/:user_uuid/address/:address_uuid" + userContactRoute = "/portal/:user_uuid/contact" + userRemoveContactRoute = "/portal/:user_uuid/contact/:contact_uuid" portalRoute = "/portal" portalBulkRoute = "/portal/bulk" contacttypeRoute = "/contacttype" @@ -41,6 +43,9 @@ func Register(r *echo.Group, cfg *config.Config, svc *applicationservice.Service r.POST(userAddressRoute, ctrl.handlePortalAddress) r.PUT(userRemoveAddressRoute, ctrl.handleRemoveAddress) + r.POST(userContactRoute, ctrl.handlePortalContact) + r.PUT(userRemoveContactRoute, ctrl.handleRemoveContact) + //Can be cached r.GET(contacttypeRoute, ctrl.handleContactType) diff --git a/server/serverconfig/authorization.go b/server/serverconfig/authorization.go index 1797e3f..d61c762 100644 --- a/server/serverconfig/authorization.go +++ b/server/serverconfig/authorization.go @@ -193,8 +193,6 @@ func (a *Config) objectRelation(object interface{}, currentUser viewmodel.User) case viewmodel.User: if obj.ID == currentUser.ID { return "[self]" - } else { - return "[other]" } } return "[other]" diff --git a/server/validation/tnc.go b/server/validation/tnc.go new file mode 100644 index 0000000..8d3c98f --- /dev/null +++ b/server/validation/tnc.go @@ -0,0 +1,183 @@ +package validation + +import ( + + "time" + "fmt" + "strconv" + + "bitbucket.org/nemt/nemt-portal-api/application/viewmodel" + "bitbucket.org/nemt/nemt-portal-api/infra/errors" + +) + +const ( + tripTypeFromVisit = "From Visit" + tripTypeToVisit = "To Visit" + tripTypeFromVisitWillCall = "From Visit / Will Call" + tripTypeRoundTrip = "Round Trip" + tripTypeRountTripWillCall = "Round Trip / Will Call" +) + +const ( + loadingTime = 30 //in minutes + minimumLoadTime = 30 //in minutes + minimumPickupTime = 10 //in minutes +) + +const ( + hoursInDay = 24 + hoursIn180Days = 24*180 + time8Hours = 8 + time10Minutes = 10 +) + +func ValidateRide(requestRide *viewmodel.RideRequest, user *viewmodel.User) []errors.ValidationError { + var result []errors.ValidationError + + //Step #1 validation + + if userID, err := strconv.Atoi(user.ID) ; err != nil || userID <= 0 { + result = append(result, errors.ValidationError{Field : "user_uuid", Message : "Step #1 - Choose a Member" }) + } + + if originID, err := strconv.Atoi(requestRide.Origin.ID) ; err != nil || originID <= 0 { + result = append (result, errors.ValidationError{Field : "origin.id", Message : "Step #1 - Choose a Pickup Address"}) + } + + if !requestRide.UserConsent { + result = append (result, errors.ValidationError{Field : "user_consent", Message : "Step #1 - Member must consent to Terms of Use"}) + } + + //Step #2 validation + + if destinationID, err := strconv.Atoi(requestRide.Destination.ID) ; err!= nil || destinationID <= 0 { + result = append (result, errors.ValidationError{Field : "destination.id", Message : "Step #2 - Choose a Provider"}) + } + + //Step #3 validation + + isVisitDayToday := requestRide.VisitDate.Day() == time.Now().Day() && requestRide.VisitDate.Month() == time.Now().Month() && requestRide.VisitDate.Year() == time.Now().Year() + before8Hours := time.Now().Add(-time.Hour*time8Hours) + + if requestRide.VisitDate == nil { + result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Choose a Date for the Visit"}) + }else{ + dayBeforeToday := time.Now().Add(-time.Hour*hoursInDay) + if requestRide.VisitDate.Before(dayBeforeToday) { + result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Visit cannot occur more than one day before today"}) + } + + dayAfter180Days := time.Now().Add(time.Hour*hoursIn180Days) + if requestRide.VisitDate.After(dayAfter180Days) { + result = append (result, errors.ValidationError{Field : "visit_date", Message : "Step #3 - Visit cannot occur more than 180 days after today"}) + } + + if requestRide.VisitTime == nil { + result = append (result, errors.ValidationError{Field : "visit_time", Message : "Step #3 - Choose a Time for the Visit"}) + }else{ + if isVisitDayToday && requestRide.VisitTime.Before(before8Hours) { + result = append (result, errors.ValidationError{Field : "visit_time", Message : "Step #3 - Visit is more than 8 hours in the past"}) + } + } + } + + //Step #4 validation + + if requestRide.TripType.Value == "" { + result = append (result, errors.ValidationError{Field : "trip_type.value", Message : "Step #4 - Choose a Trip Type"}) + } + + timeWithDurationAndLoadingTime := requestRide.VisitTime.Add(-time.Duration(requestRide.Duration)*time.Second).Add(-loadingTime*time.Minute) + after10Minutes := time.Now().Add(time.Minute*time10Minutes) + + switch requestRide.TripType.Value { + case tripTypeToVisit: + if requestRide.PickupTime == nil { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"}) + }else{ + if requestRide.PickupTime.After(*requestRide.VisitTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"}) + } + + if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time before Visit Time"}) + } + + if isVisitDayToday && requestRide.PickupTime.Before(before8Hours) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Visit cannot occour in the past"}) + } + } + + case tripTypeFromVisit : + if requestRide.PickupTime == nil { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"}) + }else{ + timeWithMinimumPickupTime := time.Now().Add(minimumPickupTime*time.Minute) + if isVisitDayToday && requestRide.PickupTime.Before(timeWithMinimumPickupTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : fmt.Sprint("Step #4 - Time must be more than %d minutes from now",minimumPickupTime)}) + } + + if requestRide.PickupTime.Before(*requestRide.VisitTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time after Visit Time"}) + } + + timeWithMinimumLoadTime := time.Now().Add(minimumLoadTime*time.Minute) + if requestRide.PickupTime.Before(timeWithMinimumLoadTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than Minimum Load Time before Visit Time"}) + } + } + + case tripTypeFromVisitWillCall: + //no special validation for this case + + case tripTypeRoundTrip: + if requestRide.PickupTime == nil { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"}) + }else{ + if requestRide.PickupTime.After(*requestRide.VisitTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"}) + } + + if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time before Visit Time"}) + } + + if isVisitDayToday && requestRide.PickupTime.Before(after10Minutes) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must be at least 10 minutes from now"}) + } + } + + if requestRide.ReturnTime == nil { + result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Choose a Pickup Time"}) + }else{ + if isVisitDayToday { + if requestRide.ReturnTime.Before(before8Hours) { + result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Return Time is more than 8 hours after Visit Time"}) + } + + if requestRide.ReturnTime.Before(after10Minutes) { + result = append (result, errors.ValidationError{Field : "return_time", Message : "Step #4 - Return Time must be at least 10 minutes from now"}) + } + } + } + + case tripTypeRountTripWillCall: + if requestRide.PickupTime == nil { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Choose a Pickup Time"}) + }else{ + if requestRide.PickupTime.After(*requestRide.VisitTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time must occur before Visit Time"}) + } + + if requestRide.PickupTime.After(timeWithDurationAndLoadingTime) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Pickup Time less than required time after Visit Time"}) + } + + if isVisitDayToday && requestRide.PickupTime.Before(before8Hours) { + result = append (result, errors.ValidationError{Field : "pickup_time", Message : "Step #4 - Visit cannot occur in the past "}) + } + } + } + return result +} \ No newline at end of file