From 196738f876aaf170b3a918fe6acadd71767d082f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 16 Feb 2026 12:15:50 +0400 Subject: [PATCH] Implement create user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- pkg/coredata/membership_profile.go | 6 + pkg/iam/iam_actions.go | 1 + pkg/iam/organization_service.go | 120 ++++++++- pkg/server/api/connect/v1/schema.graphql | 20 +- pkg/server/api/connect/v1/schema/schema.go | 294 ++++++++++++++++++++- pkg/server/api/connect/v1/types/types.go | 18 +- pkg/server/api/connect/v1/v1_resolver.go | 36 ++- 7 files changed, 484 insertions(+), 11 deletions(-) diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index 7349b39fb..1093dce59 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -22,6 +22,7 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/mail" @@ -904,6 +905,11 @@ VALUES ( _, err := conn.Exec(ctx, q, args) if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return ErrResourceAlreadyExists + } + return fmt.Errorf("cannot insert profile: %w", err) } diff --git a/pkg/iam/iam_actions.go b/pkg/iam/iam_actions.go index dca2a0a0a..4d7b91da2 100644 --- a/pkg/iam/iam_actions.go +++ b/pkg/iam/iam_actions.go @@ -54,6 +54,7 @@ const ( // Membership Profile actions ActionMembershipProfileGet = "iam:membership-profile:get" ActionMembershipProfileList = "iam:membership-profile:list" + ActionMembershipProfileCreate = "iam:membership-profile:create" ActionMembershipProfileUpdate = "iam:membership-profile:update" // Personal API Key actions diff --git a/pkg/iam/organization_service.go b/pkg/iam/organization_service.go index 6a23f8543..5de13a2dd 100644 --- a/pkg/iam/organization_service.go +++ b/pkg/iam/organization_service.go @@ -98,12 +98,24 @@ type ( Role coredata.MembershipRole } + CreateUserRequest struct { + OrganizationID gid.GID + EmailAddress mail.Addr + Role coredata.MembershipRole + FullName string + AdditionalEmailAddresses mail.Addrs + Kind coredata.MembershipProfileKind + Position *string + ContractStartDate **time.Time + ContractEndDate **time.Time + } + UpdateProfileRequest struct { ID gid.GID FullName string AdditionalEmailAddresses mail.Addrs Kind coredata.MembershipProfileKind - Position **string + Position *string ContractStartDate **time.Time ContractEndDate **time.Time } @@ -192,6 +204,22 @@ func (req UpdateOrganizationRequest) Validate() error { return v.Error() } +func (cur *CreateUserRequest) Validate() error { + v := validator.New() + + v.Check(cur.OrganizationID, "id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) + v.Check(cur.Kind, "kind", validator.OneOfSlice(coredata.MembershipProfileKinds())) + v.Check(cur.FullName, "full_name", validator.SafeTextNoNewLine(NameMaxLength)) + v.CheckEach(cur.AdditionalEmailAddresses, "additional_email_addresses", func(index int, item any) { + v.Check(item, fmt.Sprintf("additional_email_addresses[%d]", index), validator.Required(), validator.NotEmpty()) + }) + v.Check(cur.Position, "position", validator.SafeText(TitleMaxLength)) + v.Check(cur.ContractStartDate, "contract_start_date", validator.Before(cur.ContractEndDate)) + v.Check(cur.ContractEndDate, "contract_end_date", validator.After(cur.ContractStartDate)) + + return v.Error() +} + func (upr *UpdateProfileRequest) Validate() error { v := validator.New() @@ -922,6 +950,90 @@ func (s *OrganizationService) DeleteOrganization(ctx context.Context, organizati ) } +func (s *OrganizationService) CreateUser(ctx context.Context, req *CreateUserRequest) (*coredata.MembershipProfile, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + var ( + scope = coredata.NewScopeFromObjectID(req.OrganizationID) + profile *coredata.MembershipProfile + now = time.Now() + ) + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + identity := &coredata.Identity{} + if err := identity.LoadByEmail(ctx, conn, req.EmailAddress); err != nil { + if !errors.Is(err, coredata.ErrResourceNotFound) { + return fmt.Errorf("cannot load identity: %w", err) + } + + identity = &coredata.Identity{ + ID: gid.New(gid.NilTenant, coredata.IdentityEntityType), + EmailAddress: req.EmailAddress, + FullName: req.FullName, + CreatedAt: now, + UpdatedAt: now, + } + + if err := identity.Insert(ctx, conn); err != nil { + return fmt.Errorf("cannot insert identity: %w", err) + } + } + + profile = &coredata.MembershipProfile{ + ID: gid.New(req.OrganizationID.TenantID(), coredata.MembershipProfileEntityType), + FullName: req.FullName, + Kind: req.Kind, + AdditionalEmailAddresses: req.AdditionalEmailAddresses, + Position: req.Position, + // User is created inactive + State: coredata.ProfileStateInactive, + CreatedAt: now, + UpdatedAt: now, + } + + if req.ContractStartDate != nil { + profile.ContractStartDate = *req.ContractStartDate + } + + if req.ContractEndDate != nil { + profile.ContractEndDate = *req.ContractEndDate + } + + if err := profile.Insert(ctx, conn); err != nil { + if errors.Is(err, coredata.ErrResourceAlreadyExists) { + return NewUserAlreadyExistsError(identity.ID, req.OrganizationID) + } + + return fmt.Errorf("cannot insert profile: %w", err) + } + + membership := &coredata.Membership{ + ID: gid.New(req.OrganizationID.TenantID(), coredata.MembershipEntityType), + IdentityID: identity.ID, + OrganizationID: req.OrganizationID, + Role: req.Role, + CreatedAt: now, + UpdatedAt: now, + } + if err := membership.Insert(ctx, conn, scope); err != nil { + return fmt.Errorf("cannot insert membership: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return profile, nil +} + func (s *OrganizationService) UpdateProfile(ctx context.Context, req *UpdateProfileRequest) (*coredata.MembershipProfile, error) { if err := req.Validate(); err != nil { return nil, fmt.Errorf("invalid request: %w", err) @@ -941,12 +1053,8 @@ func (s *OrganizationService) UpdateProfile(ctx context.Context, req *UpdateProf profile.FullName = req.FullName profile.Kind = req.Kind - profile.AdditionalEmailAddresses = req.AdditionalEmailAddresses - - if req.Position != nil { - profile.Position = *req.Position - } + profile.Position = req.Position if req.ContractStartDate != nil { profile.ContractStartDate = *req.ContractStartDate diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 232a8eaa5..7d02f27fa 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -94,6 +94,8 @@ type Mutation { input: DeleteOrganizationHorizontalLogoInput! ): DeleteOrganizationHorizontalLogoPayload @session(required: PRESENT) + createUser(input: CreateUserInput!): CreateUserPayload + @session(required: PRESENT) inviteMember(input: InviteMemberInput!): InviteMemberPayload @session(required: PRESENT) deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload @@ -727,6 +729,18 @@ input DeleteOrganizationHorizontalLogoInput { organizationId: ID! } +input CreateUserInput { + organizationId: ID! + fullName: String! + emailAddress: EmailAddr! + role: MembershipRole! + additionalEmailAddresses: [EmailAddr!] + kind: ProfileKind! + position: String + contractStartDate: Datetime @goField(omittable: true) + contractEndDate: Datetime @goField(omittable: true) +} + input InviteMemberInput { organizationId: ID! email: EmailAddr! @@ -739,7 +753,7 @@ input UpdateProfileInput { fullName: String! additionalEmailAddresses: [EmailAddr!] kind: ProfileKind! - position: String @goField(omittable: true) + position: String contractStartDate: Datetime @goField(omittable: true) contractEndDate: Datetime @goField(omittable: true) } @@ -890,6 +904,10 @@ type DeleteOrganizationHorizontalLogoPayload { organization: Organization! } +type CreateUserPayload { + profileEdge: ProfileEdge! +} + type InviteMemberPayload { invitationEdge: InvitationEdge! } diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index d13b80cef..83fc016b7 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -119,6 +119,10 @@ type ComplexityRoot struct { Token func(childComplexity int) int } + CreateUserPayload struct { + ProfileEdge func(childComplexity int) int + } + DeleteInvitationPayload struct { DeletedInvitationID func(childComplexity int) int } @@ -203,6 +207,7 @@ type ComplexityRoot struct { CreatePersonalAPIKey func(childComplexity int, input types.CreatePersonalAPIKeyInput) int CreateSAMLConfiguration func(childComplexity int, input types.CreateSAMLConfigurationInput) int CreateSCIMConfiguration func(childComplexity int, input types.CreateSCIMConfigurationInput) int + CreateUser func(childComplexity int, input types.CreateUserInput) int DeleteInvitation func(childComplexity int, input types.DeleteInvitationInput) int DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int DeleteOrganizationHorizontalLogo func(childComplexity int, input types.DeleteOrganizationHorizontalLogoInput) int @@ -536,6 +541,7 @@ type MutationResolver interface { UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) + CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserPayload, error) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) UpdateProfile(ctx context.Context, input types.UpdateProfileInput) (*types.UpdateProfilePayload, error) @@ -760,6 +766,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.CreateSCIMConfigurationPayload.Token(childComplexity), true + case "CreateUserPayload.profileEdge": + if e.complexity.CreateUserPayload.ProfileEdge == nil { + break + } + + return e.complexity.CreateUserPayload.ProfileEdge(childComplexity), true + case "DeleteInvitationPayload.deletedInvitationId": if e.complexity.DeleteInvitationPayload.DeletedInvitationID == nil { break @@ -1129,6 +1142,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.CreateSCIMConfiguration(childComplexity, args["input"].(types.CreateSCIMConfigurationInput)), true + case "Mutation.createUser": + if e.complexity.Mutation.CreateUser == nil { + break + } + + args, err := ec.field_Mutation_createUser_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateUser(childComplexity, args["input"].(types.CreateUserInput)), true case "Mutation.deleteInvitation": if e.complexity.Mutation.DeleteInvitation == nil { break @@ -2375,6 +2399,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreatePersonalAPIKeyInput, ec.unmarshalInputCreateSAMLConfigurationInput, ec.unmarshalInputCreateSCIMConfigurationInput, + ec.unmarshalInputCreateUserInput, ec.unmarshalInputDeleteInvitationInput, ec.unmarshalInputDeleteOrganizationHorizontalLogoInput, ec.unmarshalInputDeleteOrganizationInput, @@ -2594,6 +2619,8 @@ type Mutation { input: DeleteOrganizationHorizontalLogoInput! ): DeleteOrganizationHorizontalLogoPayload @session(required: PRESENT) + createUser(input: CreateUserInput!): CreateUserPayload + @session(required: PRESENT) inviteMember(input: InviteMemberInput!): InviteMemberPayload @session(required: PRESENT) deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload @@ -3227,6 +3254,18 @@ input DeleteOrganizationHorizontalLogoInput { organizationId: ID! } +input CreateUserInput { + organizationId: ID! + fullName: String! + emailAddress: EmailAddr! + role: MembershipRole! + additionalEmailAddresses: [EmailAddr!] + kind: ProfileKind! + position: String + contractStartDate: Datetime @goField(omittable: true) + contractEndDate: Datetime @goField(omittable: true) +} + input InviteMemberInput { organizationId: ID! email: EmailAddr! @@ -3239,7 +3278,7 @@ input UpdateProfileInput { fullName: String! additionalEmailAddresses: [EmailAddr!] kind: ProfileKind! - position: String @goField(omittable: true) + position: String contractStartDate: Datetime @goField(omittable: true) contractEndDate: Datetime @goField(omittable: true) } @@ -3390,6 +3429,10 @@ type DeleteOrganizationHorizontalLogoPayload { organization: Organization! } +type CreateUserPayload { + profileEdge: ProfileEdge! +} + type InviteMemberPayload { invitationEdge: InvitationEdge! } @@ -3775,6 +3818,17 @@ func (ec *executionContext) field_Mutation_createSCIMConfiguration_args(ctx cont return args, nil } +func (ec *executionContext) field_Mutation_createUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNCreateUserInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐCreateUserInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteInvitation_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4985,6 +5039,41 @@ func (ec *executionContext) fieldContext_CreateSCIMConfigurationPayload_token(_ return fc, nil } +func (ec *executionContext) _CreateUserPayload_profileEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateUserPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_CreateUserPayload_profileEdge, + func(ctx context.Context) (any, error) { + return obj.ProfileEdge, nil + }, + nil, + ec.marshalNProfileEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileEdge, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_CreateUserPayload_profileEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreateUserPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_ProfileEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_ProfileEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProfileEdge", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _DeleteInvitationPayload_deletedInvitationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteInvitationPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7550,6 +7639,69 @@ func (ec *executionContext) fieldContext_Mutation_deleteOrganizationHorizontalLo return fc, nil } +func (ec *executionContext) _Mutation_createUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_createUser, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().CreateUser(ctx, fc.Args["input"].(types.CreateUserInput)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT") + if err != nil { + var zeroVal *types.CreateUserPayload + return zeroVal, err + } + if ec.directives.Session == nil { + var zeroVal *types.CreateUserPayload + return zeroVal, errors.New("directive session is not implemented") + } + return ec.directives.Session(ctx, nil, directive0, required) + } + + next = directive1 + return next + }, + ec.marshalOCreateUserPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐCreateUserPayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_createUser(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "profileEdge": + return ec.fieldContext_CreateUserPayload_profileEdge(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CreateUserPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createUser_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_inviteMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -15572,6 +15724,89 @@ func (ec *executionContext) unmarshalInputCreateSCIMConfigurationInput(ctx conte return it, nil } +func (ec *executionContext) unmarshalInputCreateUserInput(ctx context.Context, obj any) (types.CreateUserInput, error) { + var it types.CreateUserInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId", "fullName", "emailAddress", "role", "additionalEmailAddresses", "kind", "position", "contractStartDate", "contractEndDate"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.FullName = data + case "emailAddress": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emailAddress")) + data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) + if err != nil { + return it, err + } + it.EmailAddress = data + case "role": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) + data, err := ec.unmarshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole(ctx, v) + if err != nil { + return it, err + } + it.Role = data + case "additionalEmailAddresses": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("additionalEmailAddresses")) + data, err := ec.unmarshalOEmailAddr2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddrᚄ(ctx, v) + if err != nil { + return it, err + } + it.AdditionalEmailAddresses = data + case "kind": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("kind")) + data, err := ec.unmarshalNProfileKind2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipProfileKind(ctx, v) + if err != nil { + return it, err + } + it.Kind = data + case "position": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("position")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Position = data + case "contractStartDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contractStartDate")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.ContractStartDate = graphql.OmittableOf(data) + case "contractEndDate": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contractEndDate")) + data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) + if err != nil { + return it, err + } + it.ContractEndDate = graphql.OmittableOf(data) + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteInvitationInput(ctx context.Context, obj any) (types.DeleteInvitationInput, error) { var it types.DeleteInvitationInput asMap := map[string]any{} @@ -16431,7 +16666,7 @@ func (ec *executionContext) unmarshalInputUpdateProfileInput(ctx context.Context if err != nil { return it, err } - it.Position = graphql.OmittableOf(data) + it.Position = data case "contractStartDate": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("contractStartDate")) data, err := ec.unmarshalODatetime2ᚖtimeᚐTime(ctx, v) @@ -17156,6 +17391,45 @@ func (ec *executionContext) _CreateSCIMConfigurationPayload(ctx context.Context, return out } +var createUserPayloadImplementors = []string{"CreateUserPayload"} + +func (ec *executionContext) _CreateUserPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateUserPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, createUserPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CreateUserPayload") + case "profileEdge": + out.Values[i] = ec._CreateUserPayload_profileEdge(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var deleteInvitationPayloadImplementors = []string{"DeleteInvitationPayload"} func (ec *executionContext) _DeleteInvitationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteInvitationPayload) graphql.Marshaler { @@ -18167,6 +18441,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_deleteOrganizationHorizontalLogo(ctx, field) }) + case "createUser": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createUser(ctx, field) + }) case "inviteMember": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_inviteMember(ctx, field) @@ -21688,6 +21966,11 @@ func (ec *executionContext) unmarshalNCreateSCIMConfigurationInput2goᚗproboᚗ return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNCreateUserInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐCreateUserInput(ctx context.Context, v any) (types.CreateUserInput, error) { + res, err := ec.unmarshalInputCreateUserInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCursorKey2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey(ctx context.Context, v any) (page.CursorKey, error) { res, err := cursor.UnmarshalCursorKeyScalar(v) return res, graphql.ErrorOnPath(ctx, err) @@ -23175,6 +23458,13 @@ func (ec *executionContext) marshalOCreateSCIMConfigurationPayload2ᚖgoᚗprobo return ec._CreateSCIMConfigurationPayload(ctx, sel, v) } +func (ec *executionContext) marshalOCreateUserPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐCreateUserPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateUserPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._CreateUserPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey(ctx context.Context, v any) (*page.CursorKey, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index 446bd9007..45847cd99 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -118,6 +118,22 @@ type CreateSCIMConfigurationPayload struct { Token string `json:"token"` } +type CreateUserInput struct { + OrganizationID gid.GID `json:"organizationId"` + FullName string `json:"fullName"` + EmailAddress mail.Addr `json:"emailAddress"` + Role coredata.MembershipRole `json:"role"` + AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses,omitempty"` + Kind coredata.MembershipProfileKind `json:"kind"` + Position *string `json:"position,omitempty"` + ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"` + ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"` +} + +type CreateUserPayload struct { + ProfileEdge *ProfileEdge `json:"profileEdge"` +} + type DeleteInvitationInput struct { OrganizationID gid.GID `json:"organizationId"` InvitationID gid.GID `json:"invitationId"` @@ -555,7 +571,7 @@ type UpdateProfileInput struct { FullName string `json:"fullName"` AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses,omitempty"` Kind coredata.MembershipProfileKind `json:"kind"` - Position graphql.Omittable[*string] `json:"position,omitempty"` + Position *string `json:"position,omitempty"` ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"` ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"` } diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index 1f9997b3e..9a150fc2b 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -873,6 +873,40 @@ func (r *mutationResolver) DeleteOrganizationHorizontalLogo(ctx context.Context, panic(fmt.Errorf("not implemented: DeleteOrganizationHorizontalLogo - deleteOrganizationHorizontalLogo")) } +// CreateUser is the resolver for the createUser field. +func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserPayload, error) { + if err := r.authorize(ctx, input.OrganizationID, iam.ActionMembershipProfileCreate); err != nil { + return nil, err + } + + profile, err := r.iam.OrganizationService.CreateUser( + ctx, + &iam.CreateUserRequest{ + OrganizationID: input.OrganizationID, + EmailAddress: input.EmailAddress, + Role: input.Role, + FullName: input.FullName, + AdditionalEmailAddresses: input.AdditionalEmailAddresses, + Kind: input.Kind, + Position: input.Position, + ContractStartDate: gqlutils.UnwrapOmittable(input.ContractStartDate), + ContractEndDate: gqlutils.UnwrapOmittable(input.ContractEndDate), + }, + ) + if err != nil { + var errAlreadyExists *iam.ErrUserAlreadyExists + if errors.As(err, &errAlreadyExists) { + return nil, gqlutils.Conflictf(ctx, "user %q already exists", input.EmailAddress.String()) + } + + return nil, gqlutils.Internal(ctx) + } + + return &types.CreateUserPayload{ + ProfileEdge: types.NewProfileEdge(profile, coredata.MembershipProfileOrderFieldCreatedAt), + }, nil +} + // InviteMember is the resolver for the inviteMember field. func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionInvitationCreate); err != nil { @@ -948,7 +982,7 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input types.Update FullName: input.FullName, AdditionalEmailAddresses: input.AdditionalEmailAddresses, Kind: input.Kind, - Position: gqlutils.UnwrapOmittable(input.Position), + Position: input.Position, ContractStartDate: gqlutils.UnwrapOmittable(input.ContractStartDate), ContractEndDate: gqlutils.UnwrapOmittable(input.ContractEndDate), },