Refactor invitation system

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-10 01:11:09 +02:00
parent 0f96b8518f
commit 165eb267f2
39 changed files with 2730 additions and 1194 deletions

View File

@@ -1,81 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package console_v1
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/authz"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/statelesstoken"
"go.gearno.de/kit/httpserver"
)
type (
InvitationConfirmationRequest struct {
Token string `json:"token"`
Password string `json:"password"`
}
InvitationConfirmationResponse struct {
}
)
func InvitationConfirmationHandler(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req InvitationConfirmationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
return
}
payload, err := statelesstoken.ValidateToken[coredata.InvitationData](
authCfg.CookieSecret,
authz.TokenTypeOrganizationInvitation,
req.Token,
)
if err != nil {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid invitation token: %w", err))
return
}
user, _, err := authSvc.SignUp(r.Context(), payload.Data.Email, req.Password, payload.Data.FullName)
if err != nil {
var errUserAlreadyExists *auth.ErrUserAlreadyExists
if errors.As(err, &errUserAlreadyExists) {
user, err = authSvc.GetUserByEmail(r.Context(), payload.Data.Email)
if err != nil {
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to load existing user: %w", err))
return
}
} else {
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("failed to create user: %w", err))
return
}
}
err = authzSvc.AcceptInvitation(r.Context(), req.Token, user.ID)
if err != nil {
httpserver.RenderError(w, http.StatusInternalServerError, err)
return
}
httpserver.RenderJSON(w, http.StatusOK, InvitationConfirmationResponse{})
}
}

View File

@@ -21,6 +21,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"time"
@@ -157,7 +158,7 @@ func NewMux(
r.Post("/auth/register", SignUpHandler(authSvc, authCfg))
r.Post("/auth/login", SignInHandler(authSvc, authCfg))
r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg))
r.Post("/auth/invitation", InvitationConfirmationHandler(authSvc, authzSvc, authCfg))
r.Post("/auth/signup-from-invitation", SignupFromInvitationHandler(authSvc, authCfg))
r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg))
r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg))
@@ -316,18 +317,28 @@ func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *pro
return GetTenantService(ctx, r.proboSvc, tenantID)
}
func (r *Resolver) AuthzService(ctx context.Context, tenantID gid.TenantID) *authz.TenantAuthzService {
return GetTenantAuthzService(ctx, r.authzSvc, tenantID)
}
func GetTenantService(ctx context.Context, proboSvc *probo.Service, tenantID gid.TenantID) *probo.TenantService {
validateTenantAccess(ctx, tenantID)
return proboSvc.WithTenant(tenantID)
}
func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantID gid.TenantID) *authz.TenantAuthzService {
validateTenantAccess(ctx, tenantID)
return authzSvc.WithTenant(tenantID)
}
func validateTenantAccess(ctx context.Context, tenantID gid.TenantID) {
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
if tenantIDs == nil {
panic(fmt.Errorf("tenant not found"))
}
for _, id := range *tenantIDs {
if id == tenantID {
return proboSvc.WithTenant(tenantID)
}
if !slices.Contains(*tenantIDs, tenantID) {
panic(fmt.Errorf("tenant not found"))
}
panic(fmt.Errorf("tenant not found"))
}

View File

@@ -1465,6 +1465,10 @@ input InvitationOrder {
field: InvitationOrderField!
}
input InvitationFilter {
onlyPending: Boolean
}
input DocumentVersionFilter {
status: DocumentStatus
}
@@ -1572,6 +1576,7 @@ type Organization implements Node {
last: Int
before: CursorKey
orderBy: InvitationOrder
filter: InvitationFilter
): InvitationConnection! @goField(forceResolver: true)
connectors(
@@ -1736,8 +1741,6 @@ type User implements Node {
email: String!
createdAt: Datetime!
updatedAt: Datetime!
people(organizationId: ID!): People @goField(forceResolver: true)
}
type Membership implements Node {
@@ -1759,6 +1762,7 @@ type Invitation implements Node {
expiresAt: Datetime!
acceptedAt: Datetime
createdAt: Datetime!
organization: Organization! @goField(forceResolver: true)
}
type Connector implements Node {
@@ -2295,6 +2299,15 @@ type Viewer {
before: CursorKey
orderBy: OrganizationOrder
): OrganizationConnection! @goField(forceResolver: true)
invitations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: InvitationOrder
filter: InvitationFilter
): InvitationConnection! @goField(forceResolver: true)
}
# Connection Types
@@ -2394,13 +2407,19 @@ type TrustCenterReferenceEdge {
node: TrustCenterReference!
}
type UserConnection {
type UserConnection
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.UserConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type MembershipConnection {
type MembershipConnection
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.MembershipConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [MembershipEdge!]!
pageInfo: PageInfo!
@@ -2709,7 +2728,10 @@ type File {
updatedAt: Datetime!
}
type InvitationConnection {
type InvitationConnection
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.InvitationConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [InvitationEdge!]!
pageInfo: PageInfo!
@@ -2780,6 +2802,7 @@ type Mutation {
# User mutations
confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload!
inviteUser(input: InviteUserInput!): InviteUserPayload!
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
@@ -3540,6 +3563,10 @@ input InviteUserInput {
createPeople: Boolean!
}
input AcceptInvitationInput {
invitationId: ID!
}
input DeleteInvitationInput {
invitationId: ID!
}
@@ -4066,12 +4093,16 @@ type InviteUserPayload {
invitationEdge: InvitationEdge!
}
type AcceptInvitationPayload {
invitation: Invitation!
}
type DeleteInvitationPayload {
deletedInvitationId: ID!
}
type RemoveMemberPayload {
success: Boolean!
deletedMemberId: ID!
}
input VendorRiskAssessmentOrder {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package console_v1
import (
"encoding/json"
"fmt"
"net/http"
"github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/securecookie"
"go.gearno.de/kit/httpserver"
)
type (
SignupFromInvitationRequest struct {
Token string `json:"token"`
Password string `json:"password"`
FullName string `json:"fullName"`
}
SignupFromInvitationResponse struct {
}
)
func SignupFromInvitationHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req SignupFromInvitationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
return
}
user, session, err := authSvc.SignupFromInvitation(r.Context(), req.Token, req.Password, req.FullName)
if err != nil {
httpserver.RenderError(w, http.StatusBadRequest, err)
return
}
securecookie.Set(
w,
securecookie.DefaultConfig(
authCfg.CookieName,
authCfg.CookieSecret,
),
session.ID.String(),
)
httpserver.RenderJSON(
w,
http.StatusOK,
SignUpResponse{
User: UserResponse{
ID: user.ID,
Email: user.EmailAddress,
FullName: user.FullName,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
},
},
)
}
}

View File

@@ -16,10 +16,28 @@ package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
)
func NewInvitationConnection(p *page.Page[*coredata.Invitation, coredata.InvitationOrderField]) *InvitationConnection {
type (
InvitationConnection struct {
TotalCount int `json:"totalCount"`
Edges []*InvitationEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
Filter *InvitationFilter
}
)
func NewInvitationConnection(
p *page.Page[*coredata.Invitation, coredata.InvitationOrderField],
resolver any,
parentID gid.GID,
filter *InvitationFilter,
) *InvitationConnection {
var edges = make([]*InvitationEdge, len(p.Data))
for i := range edges {
@@ -29,6 +47,9 @@ func NewInvitationConnection(p *page.Page[*coredata.Invitation, coredata.Invitat
return &InvitationConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
Filter: filter,
}
}

View File

@@ -16,14 +16,28 @@ package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
)
type (
MembershipConnection struct {
TotalCount int `json:"totalCount"`
Edges []*MembershipEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
}
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
)
func NewMembershipConnection(p *page.Page[*coredata.Membership, coredata.MembershipOrderField]) *MembershipConnection {
func NewMembershipConnection(
p *page.Page[*coredata.Membership, coredata.MembershipOrderField],
resolver any,
parentID gid.GID,
) *MembershipConnection {
var edges = make([]*MembershipEdge, len(p.Data))
for i := range edges {
@@ -33,6 +47,8 @@ func NewMembershipConnection(p *page.Page[*coredata.Membership, coredata.Members
return &MembershipConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}

View File

@@ -16,6 +16,14 @@ type Node interface {
GetID() gid.GID
}
type AcceptInvitationInput struct {
InvitationID gid.GID `json:"invitationId"`
}
type AcceptInvitationPayload struct {
Invitation *Invitation `json:"invitation"`
}
type AssessVendorInput struct {
ID gid.GID `json:"id"`
WebsiteURL string `json:"websiteUrl"`
@@ -1200,29 +1208,28 @@ type ImportMeasurePayload struct {
}
type Invitation struct {
ID gid.GID `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Role string `json:"role"`
ExpiresAt time.Time `json:"expiresAt"`
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ID gid.GID `json:"id"`
Email string `json:"email"`
FullName string `json:"fullName"`
Role string `json:"role"`
ExpiresAt time.Time `json:"expiresAt"`
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Organization *Organization `json:"organization"`
}
func (Invitation) IsNode() {}
func (this Invitation) GetID() gid.GID { return this.ID }
type InvitationConnection struct {
TotalCount int `json:"totalCount"`
Edges []*InvitationEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type InvitationEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Invitation `json:"node"`
}
type InvitationFilter struct {
OnlyPending *bool `json:"onlyPending,omitempty"`
}
type InvitationOrder struct {
Direction page.OrderDirection `json:"direction"`
Field coredata.InvitationOrderField `json:"field"`
@@ -1280,12 +1287,6 @@ type Membership struct {
func (Membership) IsNode() {}
func (this Membership) GetID() gid.GID { return this.ID }
type MembershipConnection struct {
TotalCount int `json:"totalCount"`
Edges []*MembershipEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type MembershipEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Membership `json:"node"`
@@ -1493,7 +1494,7 @@ type RemoveMemberInput struct {
}
type RemoveMemberPayload struct {
Success bool `json:"success"`
DeletedMemberID gid.GID `json:"deletedMemberId"`
}
type Report struct {
@@ -2120,18 +2121,11 @@ type User struct {
Email string `json:"email"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
People *People `json:"people,omitempty"`
}
func (User) IsNode() {}
func (this User) GetID() gid.GID { return this.ID }
type UserConnection struct {
TotalCount int `json:"totalCount"`
Edges []*UserEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type UserEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *User `json:"node"`
@@ -2316,4 +2310,5 @@ type Viewer struct {
ID gid.GID `json:"id"`
User *User `json:"user"`
Organizations *OrganizationConnection `json:"organizations"`
Invitations *InvitationConnection `json:"invitations"`
}

View File

@@ -16,14 +16,28 @@ package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
)
type (
UserConnection struct {
TotalCount int `json:"totalCount"`
Edges []*UserEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
Resolver any
ParentID gid.GID
}
UserOrderBy OrderBy[coredata.UserOrderField]
)
func NewUserConnection(p *page.Page[*coredata.User, coredata.UserOrderField]) *UserConnection {
func NewUserConnection(
p *page.Page[*coredata.User, coredata.UserOrderField],
resolver any,
parentID gid.GID,
) *UserConnection {
var edges = make([]*UserEdge, len(p.Data))
for i := range edges {
@@ -33,6 +47,8 @@ func NewUserConnection(p *page.Page[*coredata.User, coredata.UserOrderField]) *U
return &UserConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}

View File

@@ -891,25 +891,45 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Organization is the resolver for the organization field.
func (r *invitationResolver) Organization(ctx context.Context, obj *types.Invitation) (*types.Organization, error) {
organization, err := r.authzSvc.GetOrganizationByInvitationID(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot load organization: %w", err))
}
return types.NewOrganization(organization), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) {
currentUser := UserFromContext(ctx)
if currentUser == nil {
return 0, fmt.Errorf("no authenticated user")
switch obj.Resolver.(type) {
case *organizationResolver:
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
count, err := authzSvc.CountOrganizationInvitations(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("failed to count organization invitations: %w", err))
}
return count, nil
case *viewerResolver:
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("no authenticated user"))
}
invitationFilter := coredata.NewInvitationFilter(nil)
if obj.Filter != nil {
invitationFilter = coredata.NewInvitationFilter(obj.Filter.OnlyPending)
}
count, err := r.authzSvc.CountUserInvitations(ctx, user.EmailAddress, invitationFilter)
if err != nil {
panic(fmt.Errorf("failed to count user invitations: %w", err))
}
return count, nil
}
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
if err != nil || len(memberships) == 0 {
return 0, fmt.Errorf("user has no organization memberships")
}
orgID := memberships[0].ID
count, err := r.authzSvc.CountOrganizationInvitations(ctx, orgID)
if err != nil {
return 0, fmt.Errorf("failed to count invitations: %w", err)
}
return count, nil
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Evidences is the resolver for the evidences field.
@@ -1052,23 +1072,17 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M
// TotalCount is the resolver for the totalCount field.
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
currentUser := UserFromContext(ctx)
if currentUser == nil {
return 0, fmt.Errorf("no authenticated user")
switch obj.Resolver.(type) {
case *organizationResolver:
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
count, err := authzSvc.CountOrganizationMemberships(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("failed to count organization memberships: %w", err))
}
return count, nil
default:
panic(fmt.Errorf("unknown resolver type for membership connection"))
}
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
if err != nil || len(memberships) == 0 {
return 0, fmt.Errorf("user has no organization memberships")
}
orgID := memberships[0].ID
count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID)
if err != nil {
return 0, fmt.Errorf("failed to count memberships: %w", err)
}
return count, nil
}
// CreateOrganization is the resolver for the createOrganization field.
@@ -1104,7 +1118,6 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
ctx,
probo.CreatePeopleRequest{
OrganizationID: organization.ID,
UserID: &UserFromContext(ctx).ID,
FullName: UserFromContext(ctx).FullName,
PrimaryEmailAddress: UserFromContext(ctx).EmailAddress,
AdditionalEmailAddresses: []string{},
@@ -1379,48 +1392,49 @@ func (r *mutationResolver) ConfirmEmail(ctx context.Context, input types.Confirm
// InviteUser is the resolver for the inviteUser field.
func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID)
authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID())
invitation, err := authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember))
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
panic(fmt.Errorf("failed to invite user to organization: %w", err))
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
invitation, err := r.authzSvc.InviteUserToOrganization(ctx, input.OrganizationID, input.Email, input.FullName, string(authz.RoleMember))
if err != nil {
return nil, err
}
if input.CreatePeople {
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
_, err := prb.Peoples.Create(ctx, probo.CreatePeopleRequest{
OrganizationID: input.OrganizationID,
FullName: input.FullName,
PrimaryEmailAddress: input.Email,
AdditionalEmailAddresses: []string{},
Kind: coredata.PeopleKindEmployee,
})
if err != nil {
return nil, fmt.Errorf("failed to create people record: %w", err)
}
}
return &types.InviteUserPayload{
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
}, nil
if input.CreatePeople {
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
_, err := prb.Peoples.Create(ctx, probo.CreatePeopleRequest{
OrganizationID: input.OrganizationID,
FullName: input.FullName,
PrimaryEmailAddress: input.Email,
AdditionalEmailAddresses: []string{},
Kind: coredata.PeopleKindEmployee,
})
if err != nil {
return nil, fmt.Errorf("failed to create people record: %w", err)
}
}
return nil, fmt.Errorf("organization not found")
return &types.InviteUserPayload{
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
}, nil
}
// AcceptInvitation is the resolver for the acceptInvitation field.
func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) {
user := UserFromContext(ctx)
invitation, err := r.authzSvc.AcceptInvitationByID(ctx, input.InvitationID, user.ID)
if err != nil {
panic(fmt.Errorf("failed to accept invitation: %w", err))
}
return &types.AcceptInvitationPayload{Invitation: types.NewInvitation(invitation)}, nil
}
// DeleteInvitation is the resolver for the deleteInvitation field.
func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) {
err := r.authzSvc.DeleteInvitation(ctx, input.InvitationID)
authzSvc := r.AuthzService(ctx, input.InvitationID.TenantID())
err := authzSvc.DeleteInvitation(ctx, input.InvitationID)
if err != nil {
return nil, err
panic(fmt.Errorf("failed to delete invitation: %w", err))
}
return &types.DeleteInvitationPayload{
@@ -1430,25 +1444,13 @@ func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.Del
// RemoveMember is the resolver for the removeMember field.
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
user := UserFromContext(ctx)
organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID)
authzSvc := r.AuthzService(ctx, input.OrganizationID.TenantID())
err := authzSvc.RemoveMemberFromOrganization(ctx, input.OrganizationID, input.MemberID)
if err != nil {
panic(fmt.Errorf("failed to list organizations for user: %w", err))
return nil, err
}
for _, organization := range organizations {
if organization.ID == input.OrganizationID {
err := r.authzSvc.RemoveMemberFromOrganization(ctx, input.OrganizationID, input.MemberID)
if err != nil {
return nil, err
}
return &types.RemoveMemberPayload{Success: true}, nil
}
}
return nil, fmt.Errorf("organization not found")
return &types.RemoveMemberPayload{DeletedMemberID: input.MemberID}, nil
}
// CreatePeople is the resolver for the createPeople field.
@@ -3611,16 +3613,17 @@ func (r *organizationResolver) Memberships(ctx context.Context, obj *types.Organ
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.authzSvc.GetAllOrganizationMemberships(ctx, obj.ID, cursor)
authzSvc := r.AuthzService(ctx, obj.ID.TenantID())
page, err := authzSvc.GetMembershipsByOrganizationID(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list memberships: %w", err))
}
return types.NewMembershipConnection(page), nil
return types.NewMembershipConnection(page, r, obj.ID), nil
}
// Invitations is the resolver for the invitations field.
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder) (*types.InvitationConnection, error) {
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) (*types.InvitationConnection, error) {
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
Field: coredata.InvitationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
@@ -3634,12 +3637,13 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.authzSvc.GetAllOrganizationInvitations(ctx, obj.ID, cursor)
authzSvc := r.AuthzService(ctx, obj.ID.TenantID())
page, err := authzSvc.GetInvitationsByOrganizationID(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list invitations: %w", err))
}
return types.NewInvitationConnection(page), nil
return types.NewInvitationConnection(page, r, obj.ID, filter), nil
}
// Connectors is the resolver for the connectors field.
@@ -4943,41 +4947,19 @@ func (r *trustCenterReferenceConnectionResolver) TotalCount(ctx context.Context,
return count, nil
}
// People is the resolver for the people field.
func (r *userResolver) People(ctx context.Context, obj *types.User, organizationID gid.GID) (*types.People, error) {
prb := r.ProboService(ctx, organizationID.TenantID())
people, err := prb.Peoples.GetByUserID(ctx, obj.ID)
if err != nil {
var errPeopleNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errPeopleNotFound) {
return nil, nil
}
panic(fmt.Errorf("failed to get people: %w", err))
}
return types.NewPeople(people), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *userConnectionResolver) TotalCount(ctx context.Context, obj *types.UserConnection) (int, error) {
currentUser := UserFromContext(ctx)
if currentUser == nil {
return 0, fmt.Errorf("no authenticated user")
switch obj.Resolver.(type) {
case *organizationResolver:
authzSvc := r.AuthzService(ctx, obj.ParentID.TenantID())
count, err := authzSvc.CountOrganizationUsers(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("failed to count organization users: %w", err))
}
return count, nil
default:
panic(fmt.Errorf("unknown resolver type for user connection"))
}
memberships, err := r.authzSvc.GetAllUserOrganizations(ctx, currentUser.ID)
if err != nil || len(memberships) == 0 {
return 0, fmt.Errorf("user has no organization memberships")
}
orgID := memberships[0].ID
count, err := r.authzSvc.CountOrganizationMemberships(ctx, orgID)
if err != nil {
return 0, fmt.Errorf("failed to count memberships: %w", err)
}
return count, nil
}
// Organization is the resolver for the organization field.
@@ -5353,6 +5335,35 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
return types.NewOrganizationConnection(page), nil
}
// Invitations is the resolver for the invitations field.
func (r *viewerResolver) Invitations(ctx context.Context, obj *types.Viewer, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) (*types.InvitationConnection, error) {
user := UserFromContext(ctx)
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
Field: coredata.InvitationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.InvitationOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
invitationFilter := coredata.NewInvitationFilter(nil)
if filter != nil {
invitationFilter = coredata.NewInvitationFilter(filter.OnlyPending)
}
invitations, err := r.authzSvc.GetUserInvitations(ctx, user.EmailAddress, cursor, invitationFilter)
if err != nil {
panic(fmt.Errorf("failed to list invitations for user: %w", err))
}
return types.NewInvitationConnection(invitations, r, gid.GID{}, filter), nil
}
// Asset returns schema.AssetResolver implementation.
func (r *Resolver) Asset() schema.AssetResolver { return &assetResolver{r} }
@@ -5432,6 +5443,9 @@ func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver {
return &frameworkConnectionResolver{r}
}
// Invitation returns schema.InvitationResolver implementation.
func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} }
// InvitationConnection returns schema.InvitationConnectionResolver implementation.
func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver {
return &invitationConnectionResolver{r}
@@ -5541,9 +5555,6 @@ func (r *Resolver) TrustCenterReferenceConnection() schema.TrustCenterReferenceC
return &trustCenterReferenceConnectionResolver{r}
}
// User returns schema.UserResolver implementation.
func (r *Resolver) User() schema.UserResolver { return &userResolver{r} }
// UserConnection returns schema.UserConnectionResolver implementation.
func (r *Resolver) UserConnection() schema.UserConnectionResolver { return &userConnectionResolver{r} }
@@ -5603,6 +5614,7 @@ type evidenceConnectionResolver struct{ *Resolver }
type fileResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver }
type frameworkConnectionResolver struct{ *Resolver }
type invitationResolver struct{ *Resolver }
type invitationConnectionResolver struct{ *Resolver }
type measureResolver struct{ *Resolver }
type measureConnectionResolver struct{ *Resolver }
@@ -5630,7 +5642,6 @@ type trustCenterDocumentAccessResolver struct{ *Resolver }
type trustCenterDocumentAccessConnectionResolver struct{ *Resolver }
type trustCenterReferenceResolver struct{ *Resolver }
type trustCenterReferenceConnectionResolver struct{ *Resolver }
type userResolver struct{ *Resolver }
type userConnectionResolver struct{ *Resolver }
type vendorResolver struct{ *Resolver }
type vendorBusinessAssociateAgreementResolver struct{ *Resolver }