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: 8 additions & 6 deletions models/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ 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"
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"
)

func (vt VerificationType) String() string {
Expand All @@ -31,6 +32,7 @@ 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
112 changes: 106 additions & 6 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2608,13 +2608,20 @@
"schema": {
"type": "string"
}
},
{
"name": "redirect_url",
"in": "query",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrganizationInvitationRequest"
"$ref": "#/components/schemas/CreateOrganizationInvitationQuery"
}
}
}
Expand Down Expand Up @@ -2717,7 +2724,7 @@
"Organization Invitations"
],
"summary": "Accept invitation",
"description": "Accepts an invitation to join an organization. Optionally redirects if a redirect_url is provided.",
"description": "Accepts an invitation to join an organization.",
"operationId": "acceptOrganizationInvitation",
"parameters": [
{
Expand Down Expand Up @@ -2807,6 +2814,60 @@
}
}
},
"/organizations/{organization_id}/invitations/{invitation_id}/verify": {
"get": {
"tags": [
"Organization Invitations"
],
"summary": "Verify invitation",
"description": "Verifies an invitation token.",
"operationId": "verifyOrganizationInvitation",
"parameters": [
{
"name": "token",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "redirect_url",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "organization_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "invitation_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VerifyOrganizationInvitationResponse"
}
}
}
}
}
}
},
"/organizations/{organization_id}/members": {
"get": {
"tags": [
Expand Down Expand Up @@ -4346,16 +4407,21 @@
],
"type": "object"
},
"CreateOrganizationInvitationRequest": {
"CreateOrganizationInvitationQuery": {
"properties": {
"email": {
"type": "string"
},
"redirect_url": {
"type": [
"string",
"null"
]
}
},
"type": "object"
},
"CreateOrganizationInvitationRequest": {
"properties": {
"email": {
"type": "string"
},
"role": {
"type": "string"
Expand Down Expand Up @@ -5239,6 +5305,29 @@
],
"type": "object"
},
"OrganizationSummary": {
"properties": {
"id": {
"type": "string"
},
"logo": {
"type": [
"null",
"string"
]
},
"name": {
"type": "string"
},
"owner_id": {
"type": "string"
},
"slug": {
"type": "string"
}
},
"type": "object"
},
"OrganizationTeam": {
"properties": {
"created_at": {
Expand Down Expand Up @@ -6451,6 +6540,17 @@
],
"type": "object"
},
"VerifyOrganizationInvitationResponse": {
"properties": {
"invitation": {
"$ref": "#/components/schemas/OrganizationInvitation"
},
"organization": {
"$ref": "#/components/schemas/OrganizationSummary"
}
},
"type": "object"
},
"VerifyTOTPRequest": {
"properties": {
"code": {
Expand Down
8 changes: 6 additions & 2 deletions plugins/organizations/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func (a *API) DeleteOrganization(ctx context.Context, actor *models.Actor, organ

// Invitations

func (a *API) CreateInvitation(ctx context.Context, actor *models.Actor, organizationID string, request types.CreateOrganizationInvitationRequest) (*types.OrganizationInvitation, error) {
return a.useCases.CreateOrganizationInvitation(ctx, actor, organizationID, request)
func (a *API) CreateInvitation(ctx context.Context, actor *models.Actor, organizationID string, request types.CreateOrganizationInvitationRequest, redirectURL string) (*types.OrganizationInvitation, error) {
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) {
Expand All @@ -70,6 +70,10 @@ 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
15 changes: 12 additions & 3 deletions plugins/organizations/constants/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import (
)

var (
ErrOrganizationsQuotaExceeded = errors.New("organizations quota exceeded")
ErrMembersQuotaExceeded = errors.New("members quota exceeded")
ErrInvitationsQuotaExceeded = errors.New("invitations quota exceeded")
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")
)

func HandleError(err error, reqCtx *models.RequestContext) {
Expand All @@ -20,6 +23,12 @@ 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
}

if status != 0 {
Expand Down
36 changes: 36 additions & 0 deletions plugins/organizations/handlers/handler_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package handlers
import (
"context"

"github.com/stretchr/testify/mock"

internaltests "github.com/Authula/authula/internal/tests"
"github.com/Authula/authula/models"
orgservices "github.com/Authula/authula/plugins/organizations/services"
orgtests "github.com/Authula/authula/plugins/organizations/tests"
Expand All @@ -19,13 +22,30 @@ func (a *noopAuthorizer) AuthorizeOrganizationAccess(_ context.Context, _ *model
return nil
}

func defaultMockUserService() *internaltests.MockUserService {
svc := &internaltests.MockUserService{}
svc.On("GetByID", mock.Anything, mock.Anything).Return((*models.User)(nil), nil).Maybe()
svc.On("GetByEmail", mock.Anything, mock.Anything).Return((*models.User)(nil), nil).Maybe()
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,
&orgtests.MockOrganizationInvitationService{},
&orgtests.MockOrganizationMemberService{},
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}
Expand All @@ -37,6 +57,10 @@ func newInvitationUseCases(svc orgservices.OrganizationInvitationService) *orgus
&orgtests.MockOrganizationMemberService{},
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}
Expand All @@ -48,6 +72,10 @@ func newMemberUseCases(svc orgservices.OrganizationMemberService) *orgusecases.U
svc,
&orgtests.MockOrganizationTeamService{},
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}
Expand All @@ -59,6 +87,10 @@ func newTeamUseCases(svc orgservices.OrganizationTeamService) *orgusecases.UseCa
&orgtests.MockOrganizationMemberService{},
svc,
&orgtests.MockOrganizationTeamMemberService{},
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}
Expand All @@ -70,6 +102,10 @@ func newTeamMemberUseCases(svc orgservices.OrganizationTeamMemberService) *orgus
&orgtests.MockOrganizationMemberService{},
&orgtests.MockOrganizationTeamService{},
svc,
defaultMockUserService(),
defaultMockVerificationService(),
&internaltests.MockTokenService{},
&models.Config{},
&noopAuthorizer{},
)
}
4 changes: 2 additions & 2 deletions plugins/organizations/handlers/organization_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (h *CreateOrganizationHandler) Handle() http.HandlerFunc {

var request types.CreateOrganizationRequest
if err := util.ParseJSON(r, &request); err != nil {
reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{"message": "invalid request body"})
reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{"message": err.Error()})
reqCtx.Handled = true
return
}
Expand Down Expand Up @@ -102,7 +102,7 @@ func (h *UpdateOrganizationHandler) Handle() http.HandlerFunc {

var request types.UpdateOrganizationRequest
if err := util.ParseJSON(r, &request); err != nil {
reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{"message": "invalid request body"})
reqCtx.SetJSONResponse(http.StatusUnprocessableEntity, map[string]any{"message": err.Error()})
reqCtx.Handled = true
return
}
Expand Down
4 changes: 2 additions & 2 deletions plugins/organizations/handlers/organization_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestCreateOrganizationHandler(t *testing.T) {
userID: new("user-1"),
body: []byte("{invalid"),
expectedStatus: http.StatusUnprocessableEntity,
expectedMessage: "invalid request body",
expectedMessage: "invalid character 'i' looking for beginning of object key string",
},
{
name: "unprocessable_entity",
Expand Down Expand Up @@ -356,7 +356,7 @@ func TestUpdateOrganizationHandler(t *testing.T) {
organizationID: "org-1",
body: []byte("{invalid"),
expectedStatus: http.StatusUnprocessableEntity,
expectedMessage: "invalid request body",
expectedMessage: "invalid character 'i' looking for beginning of object key string",
},
{
name: "unprocessable_entity",
Expand Down
Loading