Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions models/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import (
type VerificationType string

const (
TypeEmailVerification VerificationType = "email_verification"
TypePasswordResetRequest VerificationType = "password_reset_request"
TypeEmailResetRequest VerificationType = "email_reset_request"
TypeMagicLinkSignInRequest VerificationType = "magic_link_sign_in_request"
TypeMagicLinkExchangeCode VerificationType = "magic_link_exchange_code"
TypeTOTPPendingAuth VerificationType = "totp_pending_auth"
TypeOrganizationInvitationVerify VerificationType = "organization_invitation_verify"
TypeEmailVerification VerificationType = "email_verification"
TypePasswordResetRequest VerificationType = "password_reset_request"
TypeEmailResetRequest VerificationType = "email_reset_request"
TypeMagicLinkSignInRequest VerificationType = "magic_link_sign_in_request"
TypeMagicLinkExchangeCode VerificationType = "magic_link_exchange_code"
TypeTOTPPendingAuth VerificationType = "totp_pending_auth"
)

func (vt VerificationType) String() string {
Expand All @@ -32,7 +31,6 @@ func (VerificationType) PrepareJSONSchema(schema *jsonschema.Schema) error {
string(TypeMagicLinkSignInRequest),
string(TypeMagicLinkExchangeCode),
string(TypeTOTPPendingAuth),
string(TypeOrganizationInvitationVerify),
}
schema.WithDescription("The type of the verification")
return nil
Expand Down
12 changes: 4 additions & 8 deletions plugins/organizations/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ func (a *API) CreateInvitation(ctx context.Context, actor *models.Actor, organiz
return a.useCases.CreateOrganizationInvitation(ctx, actor, organizationID, request, redirectURL)
}

func (a *API) GetInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) {
return a.useCases.GetOrganizationInvitation(ctx, actor, organizationID, invitationID)
func (a *API) GetAllInvitations(ctx context.Context, actor *models.Actor, organizationID string) ([]types.GetOrganizationInvitationResponse, error) {
return a.useCases.GetAllOrganizationInvitations(ctx, actor, organizationID)
}

func (a *API) GetAllInvitations(ctx context.Context, actor *models.Actor, organizationID string) ([]types.OrganizationInvitation, error) {
return a.useCases.GetAllOrganizationInvitations(ctx, actor, organizationID)
func (a *API) GetInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.GetOrganizationInvitationResponse, error) {
return a.useCases.GetOrganizationInvitation(ctx, actor, organizationID, invitationID)
}

func (a *API) RevokeInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string) (*types.OrganizationInvitation, error) {
Expand All @@ -70,10 +70,6 @@ func (a *API) RejectInvitation(ctx context.Context, actor *models.Actor, organiz
return a.useCases.RejectOrganizationInvitation(ctx, actor, organizationID, invitationID)
}

func (a *API) VerifyInvitation(ctx context.Context, actor *models.Actor, organizationID string, invitationID string, token string) (*types.VerifyOrganizationInvitationResponse, error) {
return a.useCases.VerifyOrganizationInvitation(ctx, actor, organizationID, invitationID, token)
}

// Members

func (a *API) AddMember(ctx context.Context, actor *models.Actor, organizationID string, request types.AddOrganizationMemberRequest) (*types.OrganizationMember, error) {
Expand Down
14 changes: 4 additions & 10 deletions plugins/organizations/constants/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ import (
)

var (
ErrOrganizationsQuotaExceeded = errors.New("organizations quota exceeded")
ErrMembersQuotaExceeded = errors.New("members quota exceeded")
ErrInvitationsQuotaExceeded = errors.New("invitations quota exceeded")
ErrInvitationVerificationFailed = errors.New("verification token not found or already used")
ErrInvitationVerificationExpired = errors.New("verification token has expired")
ErrInvitationEmailMismatch = errors.New("this invitation was sent to a different email address")
ErrOrganizationsQuotaExceeded = errors.New("organizations quota exceeded")
ErrMembersQuotaExceeded = errors.New("members quota exceeded")
ErrInvitationsQuotaExceeded = errors.New("invitations quota exceeded")
ErrInvitationEmailMismatch = errors.New("this invitation was sent to a different email address")
)

func HandleError(err error, reqCtx *models.RequestContext) {
Expand All @@ -23,10 +21,6 @@ func HandleError(err error, reqCtx *models.RequestContext) {
switch err {
case ErrOrganizationsQuotaExceeded, ErrMembersQuotaExceeded, ErrInvitationsQuotaExceeded:
status = http.StatusTooManyRequests
case ErrInvitationVerificationFailed:
status = http.StatusNotFound
case ErrInvitationVerificationExpired:
status = http.StatusGone
case ErrInvitationEmailMismatch:
status = http.StatusForbidden
}
Expand Down
20 changes: 2 additions & 18 deletions plugins/organizations/handlers/handler_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ func defaultMockUserService() *internaltests.MockUserService {
return svc
}

func defaultMockVerificationService() *internaltests.MockVerificationService {
svc := &internaltests.MockVerificationService{}
svc.On("DeleteByUserIDAndType", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
return svc
}

func newOrgUseCases(svc orgservices.OrganizationService) *orgusecases.UseCases {
return orgusecases.NewUseCases(
svc,
Expand All @@ -43,23 +37,19 @@ func newOrgUseCases(svc orgservices.OrganizationService) *orgusecases.UseCases {
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}

func newInvitationUseCases(svc orgservices.OrganizationInvitationService) *orgusecases.UseCases {
func newInvitationUseCases(orgSvc orgservices.OrganizationService, svc orgservices.OrganizationInvitationService) *orgusecases.UseCases {
return orgusecases.NewUseCases(
&orgtests.MockOrganizationService{},
orgSvc,
svc,
&orgtests.MockOrganizationMemberService{},
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
Expand All @@ -73,8 +63,6 @@ func newMemberUseCases(svc orgservices.OrganizationMemberService) *orgusecases.U
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
Expand All @@ -88,8 +76,6 @@ func newTeamUseCases(svc orgservices.OrganizationTeamService) *orgusecases.UseCa
svc,
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
Expand All @@ -103,8 +89,6 @@ func newTeamMemberUseCases(svc orgservices.OrganizationTeamMemberService) *orgus
&orgtests.MockOrganizationTeamService{},
svc,
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
Expand Down
51 changes: 2 additions & 49 deletions plugins/organizations/handlers/organization_invitation_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ func (h *GetOrganizationInvitationHandler) Handle() http.HandlerFunc {

organizationID := r.PathValue("organization_id")
invitationID := r.PathValue("invitation_id")
invitation, err := h.UseCases.GetOrganizationInvitation(ctx, actor, organizationID, invitationID)
resp, err := h.UseCases.GetOrganizationInvitation(ctx, actor, organizationID, invitationID)
if err != nil {
orgconstants.HandleError(err, reqCtx)
return
}

reqCtx.SetJSONResponse(http.StatusOK, invitation)
reqCtx.SetJSONResponse(http.StatusOK, resp)
}
}

Expand Down Expand Up @@ -151,53 +151,6 @@ func (h *AcceptOrganizationInvitationHandler) Handle() http.HandlerFunc {
}
}

type VerifyOrganizationInvitationHandler struct {
UseCases *orgusecases.UseCases
TrustedOrigins []string
}

func (h *VerifyOrganizationInvitationHandler) Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqCtx, _ := models.GetRequestContext(ctx)
actor := reqCtx.Actor

organizationID := r.PathValue("organization_id")
invitationID := r.PathValue("invitation_id")

token := r.URL.Query().Get("token")
if token == "" {
reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{"message": "token is required"})
reqCtx.Handled = true
return
}

resp, err := h.UseCases.VerifyOrganizationInvitation(ctx, actor, organizationID, invitationID, token)
if err != nil {
orgconstants.HandleError(err, reqCtx)
return
}

redirectURL := r.URL.Query().Get("redirect_url")
if redirectURL != "" {
validatedURL, err := util.IsTrustedCallbackURL(redirectURL, h.TrustedOrigins)
if err != nil {
reqCtx.SetJSONResponse(http.StatusBadRequest, map[string]any{"message": err.Error()})
reqCtx.Handled = true
return
}
q := validatedURL.Query()
q.Set("invite_id", invitationID)
validatedURL.RawQuery = q.Encode()
reqCtx.RedirectURL = validatedURL.String()
reqCtx.ResponseStatus = http.StatusFound
return
}

reqCtx.SetJSONResponse(http.StatusOK, resp)
}
}

type RejectOrganizationInvitationHandler struct {
UseCases *orgusecases.UseCases
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

type organizationInvitationHandlerFixture struct {
service *orgtests.MockOrganizationInvitationService
orgSvc *orgtests.MockOrganizationService
}

type organizationInvitationHandlerCase struct {
Expand All @@ -35,7 +36,10 @@ type organizationInvitationHandlerCase struct {
}

func newOrganizationInvitationHandlerFixture() *organizationInvitationHandlerFixture {
return &organizationInvitationHandlerFixture{service: &orgtests.MockOrganizationInvitationService{}}
return &organizationInvitationHandlerFixture{
service: &orgtests.MockOrganizationInvitationService{},
orgSvc: &orgtests.MockOrganizationService{},
}
}

func (f *organizationInvitationHandlerFixture) newRequest(t *testing.T, method, path string, body []byte, userID *string, organizationID, invitationID string) (*http.Request, *httptest.ResponseRecorder, *models.RequestContext) {
Expand Down Expand Up @@ -84,6 +88,7 @@ func runOrganizationInvitationHandlerCases(t *testing.T, method, path string, bu
tt.checkResponse(t, reqCtx)
}
fixture.service.AssertExpectations(t)
fixture.orgSvc.AssertExpectations(t)
})
}
}
Expand All @@ -92,7 +97,7 @@ func TestCreateOrganizationInvitationHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodPost, "/organizations/org-1/invitations", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&CreateOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&CreateOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "missing_user",
Expand Down Expand Up @@ -155,7 +160,7 @@ func TestGetAllOrganizationInvitationsHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodGet, "/organizations/org-1/invitations", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&GetAllOrganizationInvitationsHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&GetAllOrganizationInvitationsHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "missing_user",
Expand All @@ -168,7 +173,7 @@ func TestGetAllOrganizationInvitationsHandler(t *testing.T) {
userID: new("user-1"),
organizationID: "org-1",
prepare: func(fixture *organizationInvitationHandlerFixture) {
fixture.service.On("GetAllOrganizationInvitations", mock.Anything, "user-1", "org-1").Return(([]orgtypes.OrganizationInvitation)(nil), errors.New("some error")).Once()
fixture.service.On("GetAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1").Return(([]orgtypes.GetOrganizationInvitationResponse)(nil), errors.New("some error")).Once()
},
expectedStatus: http.StatusBadRequest,
expectedMessage: "some error",
Expand All @@ -178,14 +183,20 @@ func TestGetAllOrganizationInvitationsHandler(t *testing.T) {
userID: new("user-1"),
organizationID: "org-1",
prepare: func(fixture *organizationInvitationHandlerFixture) {
fixture.service.On("GetAllOrganizationInvitations", mock.Anything, "user-1", "org-1").Return([]orgtypes.OrganizationInvitation{{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: orgtypes.OrganizationInvitationStatusPending}}, nil).Once()
fixture.service.On("GetAllOrganizationInvitationsByOrgIDWithOrg", mock.Anything, "org-1").Return([]orgtypes.GetOrganizationInvitationResponse{
{
Invitation: &orgtypes.OrganizationInvitation{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: orgtypes.OrganizationInvitationStatusPending},
Organization: orgtypes.OrganizationSummary{ID: "org-1", Name: "Acme Corp", Slug: "acme"},
},
}, nil).Once()
},
expectedStatus: http.StatusOK,
checkResponse: func(t *testing.T, reqCtx *models.RequestContext) {
invitations := internaltests.DecodeResponseJSON[[]orgtypes.OrganizationInvitation](t, reqCtx)
require.Len(t, invitations, 1)
assert.Equal(t, "inv-1", invitations[0].ID)
assert.Equal(t, "org-1", invitations[0].OrganizationID)
resp := internaltests.DecodeResponseJSON[[]orgtypes.GetOrganizationInvitationResponse](t, reqCtx)
require.Len(t, resp, 1)
assert.Equal(t, "inv-1", resp[0].Invitation.ID)
assert.Equal(t, "org-1", resp[0].Invitation.OrganizationID)
assert.Equal(t, "Acme Corp", resp[0].Organization.Name)
},
},
})
Expand All @@ -195,7 +206,7 @@ func TestGetOrganizationInvitationHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodGet, "/organizations/org-1/invitations/inv-1", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&GetOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&GetOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "missing_user",
Expand All @@ -210,7 +221,7 @@ func TestGetOrganizationInvitationHandler(t *testing.T) {
organizationID: "org-1",
invitationID: "inv-1",
prepare: func(fixture *organizationInvitationHandlerFixture) {
fixture.service.On("GetOrganizationInvitationByID", mock.Anything, "inv-1").Return((*orgtypes.OrganizationInvitation)(nil), coreerrors.ErrNotFound).Once()
fixture.service.On("GetOrganizationInvitationByIDWithOrg", mock.Anything, "inv-1").Return((*orgtypes.GetOrganizationInvitationResponse)(nil), coreerrors.ErrNotFound).Once()
},
expectedStatus: http.StatusNotFound,
expectedMessage: "not found",
Expand All @@ -221,14 +232,20 @@ func TestGetOrganizationInvitationHandler(t *testing.T) {
organizationID: "org-1",
invitationID: "inv-1",
prepare: func(fixture *organizationInvitationHandlerFixture) {
fixture.service.On("GetOrganizationInvitationByID", mock.Anything, "inv-1").Return(&orgtypes.OrganizationInvitation{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: orgtypes.OrganizationInvitationStatusPending}, nil).Once()
fixture.service.On("GetOrganizationInvitation", mock.Anything, "user-1", "org-1", "inv-1").Return(&orgtypes.OrganizationInvitation{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: orgtypes.OrganizationInvitationStatusPending}, nil).Once()
fixture.service.On("GetOrganizationInvitationByIDWithOrg", mock.Anything, "inv-1").Return(&orgtypes.GetOrganizationInvitationResponse{
Invitation: &orgtypes.OrganizationInvitation{ID: "inv-1", OrganizationID: "org-1", Email: "user@example.com", Role: "member", Status: orgtypes.OrganizationInvitationStatusPending},
Organization: orgtypes.OrganizationSummary{ID: "org-1", Name: "Acme Corp", Slug: "acme"},
}, nil).Once()
},
expectedStatus: http.StatusOK,
checkResponse: func(t *testing.T, reqCtx *models.RequestContext) {
invitation := internaltests.DecodeResponseJSON[orgtypes.OrganizationInvitation](t, reqCtx)
assert.Equal(t, "inv-1", invitation.ID)
assert.Equal(t, "org-1", invitation.OrganizationID)
resp := internaltests.DecodeResponseJSON[orgtypes.GetOrganizationInvitationResponse](t, reqCtx)
require.NotNil(t, resp)
assert.Equal(t, "inv-1", resp.Invitation.ID)
assert.Equal(t, "org-1", resp.Invitation.OrganizationID)
assert.Equal(t, "org-1", resp.Organization.ID)
assert.Equal(t, "Acme Corp", resp.Organization.Name)
assert.Equal(t, "acme", resp.Organization.Slug)
},
},
})
Expand All @@ -238,7 +255,7 @@ func TestRevokeOrganizationInvitationHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodPatch, "/organizations/org-1/invitations/inv-1", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&RevokeOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&RevokeOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "missing_user",
Expand Down Expand Up @@ -280,7 +297,7 @@ func TestAcceptOrganizationInvitationHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodPost, "/organizations/org-1/invitations/inv-1/accept", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&AcceptOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&AcceptOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "redirect_url",
Expand Down Expand Up @@ -350,7 +367,7 @@ func TestRejectOrganizationInvitationHandler(t *testing.T) {
t.Parallel()

runOrganizationInvitationHandlerCases(t, http.MethodPost, "/organizations/org-1/invitations/inv-1/reject", func(fixture *organizationInvitationHandlerFixture) http.HandlerFunc {
return (&RejectOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.service)}).Handle()
return (&RejectOrganizationInvitationHandler{UseCases: newInvitationUseCases(fixture.orgSvc, fixture.service)}).Handle()
}, []organizationInvitationHandlerCase{
{
name: "missing_user",
Expand Down
Loading