diff --git a/apps/console/src/__generated__/iam/ActivateAccountPageMutation.graphql.ts b/apps/console/src/__generated__/iam/ActivateAccountPageMutation.graphql.ts index b0fe3c796..7952f14e3 100644 --- a/apps/console/src/__generated__/iam/ActivateAccountPageMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/ActivateAccountPageMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,7 +10,7 @@ import { ConcreteRequest } from 'relay-runtime'; export type ActivateAccountInput = { - password: string; + password?: string | null | undefined; token: string; }; export type ActivateAccountPageMutation$variables = { diff --git a/apps/console/src/pages/iam/auth/ActivateAccountPage.tsx b/apps/console/src/pages/iam/auth/ActivateAccountPage.tsx index 1495c6b12..4f96b8433 100644 --- a/apps/console/src/pages/iam/auth/ActivateAccountPage.tsx +++ b/apps/console/src/pages/iam/auth/ActivateAccountPage.tsx @@ -1,14 +1,13 @@ -import { formatError } from "@probo/helpers"; +import { formatError, type GraphQLError } from "@probo/helpers"; import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; -import { Button, Field, useToast } from "@probo/ui"; +import { useToast } from "@probo/ui"; +import { useCallback, useEffect, useRef } from "react"; import { useMutation } from "react-relay"; import { Link, useNavigate, useSearchParams } from "react-router"; import { graphql } from "relay-runtime"; -import { z } from "zod"; import type { ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql"; -import { useFormWithSchema } from "#/hooks/useFormWithSchema"; const activateAccountMutation = graphql` mutation ActivateAccountPageMutation( @@ -22,64 +21,45 @@ const activateAccountMutation = graphql` } `; -const schema = z.object({ - password: z.string().min(8), - fullName: z.string().min(2), -}); - -type FormData = z.infer; - export default function ActivateAccountPage() { const { __ } = useTranslate(); const { toast } = useToast(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const fullNameFromParams = searchParams.get("fullName") || ""; + const submittedRef = useRef(false); - usePageTitle(__("Sign up")); - - const { register, handleSubmit, formState } = useFormWithSchema(schema, { - defaultValues: { - password: "", - fullName: fullNameFromParams, - }, - }); + usePageTitle(__("Activate Account")); const [activateAccount] = useMutation(activateAccountMutation); - const onSubmit = (data: FormData) => { - const token = searchParams.get("token"); - if (!token) { - toast({ - title: __("Signup failed"), - description: __("Invalid or missing invitation token"), - variant: "error", - }); - return; - } + const handleActivateAccount = useCallback((token: string) => { + if (submittedRef.current) return; activateAccount({ variables: { - input: { - token, - password: data.password, - fullName: data.fullName, - }, + input: { token }, }, - onCompleted: (_, e) => { - if (e) { + onCompleted: (_, errors: GraphQLError[] | null) => { + if (errors) { + for (const err of errors) { + if (err.extensions?.code === "ALREADY_AUTHENTICATED") { + window.location.href = "/"; + return; + } + } toast({ - title: __("Signup failed"), - description: formatError(__("Signup failed"), e), + title: __("Activation failed"), + description: formatError(__("Activation failed"), errors), variant: "error", }); + return; } toast({ title: __("Success"), description: __( - "Account created successfully. Please accept your invitation to join the organization.", + "Account activated successfully.", ), variant: "success", }); @@ -87,60 +67,37 @@ export default function ActivateAccountPage() { }, onError: (e) => { toast({ - title: __("Signup failed"), + title: __("Activation failed"), description: e.message, variant: "error", }); }, }); - }; + }, [__, toast, activateAccount, navigate]); + + useEffect(() => { + const token = searchParams.get("token"); + if (!submittedRef.current && token) { + void handleActivateAccount(token.trim()); + submittedRef.current = true; + } + }, [handleActivateAccount, searchParams]); return (
-

{__("Create your account")}

+

{__("Account Activation")}

- {__("Set your password to join the organization")} + {__("Activating your account…")}

- -
void handleSubmit(onSubmit)(e)} className="space-y-4"> - - - - - - - -
-

- {__("Already have an account?")} - {" "} - - {__("Log in here")} - -

+
+ + {__("Go back")} +
); diff --git a/packages/emails/src/Invitation.tsx b/packages/emails/src/Invitation.tsx index b10fef7b2..6895b0350 100644 --- a/packages/emails/src/Invitation.tsx +++ b/packages/emails/src/Invitation.tsx @@ -6,17 +6,17 @@ export const Invitation = () => { return ( - You have been invited to join organization {'{{.OrganizationName}}'} on Probo. Click the button below to accept the invitation: + You have been invited to join organization {'{{.OrganizationName}}'} on Probo. Click the button below to activate your account:
- If you don't want to accept the invitation, you can ignore this email. + If you don't want to do so, you can ignore this email.
); diff --git a/packages/emails/templates/invitation.txt b/packages/emails/templates/invitation.txt index 2814e6d27..14c31c721 100644 --- a/packages/emails/templates/invitation.txt +++ b/packages/emails/templates/invitation.txt @@ -2,11 +2,11 @@ Probo Hi {{.RecipientFullName}}, -You have been invited to join organization {{.OrganizationName}} on Probo. Click the link below to accept the invitation: +You have been invited to join organization {{.OrganizationName}} on Probo. Click the link below to activate your account: {{.InvitationUrl}} -If you don't want to accept the invitation, you can ignore this email. +If you don't want to do so, you can ignore this email. {{.SenderCompanyHeadquarterAddress}} Powered By Probo diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go index ac606bf67..4277d198e 100644 --- a/pkg/coredata/invitation.go +++ b/pkg/coredata/invitation.go @@ -293,7 +293,6 @@ func (i *Invitations) ExpireByUserID( expires_at = NOW() WHERE user_id = @user_id - AND organization_id = @organization_id AND %s AND %s ` diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 8dd7ab256..854277580 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -47,7 +47,6 @@ type ( CreateIdentityFromInvitationRequest struct { InvitationToken string - Password string } LoadOrCreateIdentityRequest struct { @@ -92,7 +91,6 @@ func (req CreateIdentityFromInvitationRequest) Validate() error { v := validator.New() v.Check(req.InvitationToken, "invitationToken", validator.NotEmpty()) - v.Check(req.Password, "password", PasswordValidator()) return v.Error() } @@ -153,11 +151,6 @@ func (s *AuthService) ActivateAccount( now = time.Now() ) - hashedPassword, err := s.hp.HashPassword([]byte(req.Password)) - if err != nil { - return nil, nil, fmt.Errorf("cannot hash password: %w", err) - } - err = s.pg.WithTx( ctx, func(tx pg.Conn) error { @@ -197,7 +190,6 @@ func (s *AuthService) ActivateAccount( return fmt.Errorf("cannot load identity: %w", err) } - identity.HashedPassword = hashedPassword identity.EmailAddressVerified = true identity.UpdatedAt = now diff --git a/pkg/iam/errors.go b/pkg/iam/errors.go index dbe5b6c7b..396c19460 100644 --- a/pkg/iam/errors.go +++ b/pkg/iam/errors.go @@ -326,15 +326,15 @@ func (e ErrInvitationNotDeleted) Error() string { return fmt.Sprintf("cannot delete invitation %q in %q status", e.InvitationID, e.Status) } -type ErrPasswordRequired struct { +type ErrPasswordAuthenticationRequired struct { Reason string } -func NewPasswordRequiredError(reason string) *ErrPasswordRequired { - return &ErrPasswordRequired{Reason: reason} +func NewPasswordAuthenticationRequiredError(reason string) *ErrPasswordAuthenticationRequired { + return &ErrPasswordAuthenticationRequired{Reason: reason} } -func (e *ErrPasswordRequired) Error() string { +func (e *ErrPasswordAuthenticationRequired) Error() string { return fmt.Sprintf("password authentication required: %s", e.Reason) } diff --git a/pkg/iam/session_service.go b/pkg/iam/session_service.go index 04d035dff..d5fc57e93 100644 --- a/pkg/iam/session_service.go +++ b/pkg/iam/session_service.go @@ -573,16 +573,23 @@ func (s SessionService) AssumeOrganizationSession( return fmt.Errorf("cannot load SAML configuration: %w", err) } - if err == nil && samlConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired { - if rootSession.AuthMethod != coredata.AuthMethodSAML { - return NewSAMLAuthenticationRequiredError("policy_requirement") + if err == nil { + switch samlConfig.EnforcementPolicy { + case coredata.SAMLEnforcementPolicyRequired: + if rootSession.AuthMethod != coredata.AuthMethodSAML { + return NewSAMLAuthenticationRequiredError("policy_requirement") + } + case coredata.SAMLEnforcementPolicyOptional: + // SAML is optional: both PASSWORD and SAML root sessions are allowed. + } + } else { + switch rootSession.AuthMethod { + case coredata.AuthMethodPassword: + case coredata.AuthMethodSAML: + // No (or non-required) SAML configuration: require a password-authenticated or magic-link root session + // (eg. when switching into a password-based org from a SAML login) + return NewPasswordAuthenticationRequiredError("password_authentication_required") } - } else if err == nil && samlConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyOptional { - // SAML is optional: both PASSWORD and SAML root sessions are allowed. - } else if rootSession.AuthMethod != coredata.AuthMethodPassword { - // No (or non-required) SAML configuration: require a password-authenticated root session - // (eg. when switching into a password-based org from a SAML login). - return NewPasswordRequiredError("password_authentication_required") } tenantID := scope.GetTenantID() diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 589e4a3a2..cfbe0389e 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -141,6 +141,7 @@ type Identity implements Node { last: Int before: CursorKey orderBy: ProfileOrder + filter: ProfileFilter ): ProfileConnection @goField(forceResolver: true) sessions( @@ -512,6 +513,11 @@ enum ProfileOrderField ) } +input ProfileFilter { + excludeContractEnded: Boolean + state: ProfileState +} + input ProfileOrder @goModel( model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy" @@ -628,7 +634,6 @@ input SignUpInput { input ActivateAccountInput { token: String! - password: String! } input ForgotPasswordInput { @@ -794,6 +799,10 @@ type ActivateAccountPayload { profile: Profile } +type CreatePasswordPayload { + success: Boolean! +} + type ForgotPasswordPayload { success: Boolean! } diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 47aa31144..f61a68df9 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -108,6 +108,10 @@ type ComplexityRoot struct { Organization func(childComplexity int) int } + CreatePasswordPayload struct { + Success func(childComplexity int) int + } + CreatePersonalAPIKeyPayload struct { PersonalAPIKeyEdge func(childComplexity int) int Token func(childComplexity int) int @@ -159,7 +163,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int Permission func(childComplexity int, action string) int PersonalAPIKeys func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int - Profiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) int + Profiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy, filter *types.ProfileFilter) int Sessions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) int UpdatedAt func(childComplexity int) int } @@ -499,7 +503,7 @@ type ConnectorResolver interface { Permission(ctx context.Context, obj *types.Connector, action string) (bool, error) } type IdentityResolver interface { - Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) + Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy, filter *types.ProfileFilter) (*types.ProfileConnection, error) Sessions(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) (*types.SessionConnection, error) PersonalAPIKeys(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PersonalAPIKeyConnection, error) SsoLoginURL(ctx context.Context, obj *types.Identity) (*string, error) @@ -727,6 +731,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.CreateOrganizationPayload.Organization(childComplexity), true + case "CreatePasswordPayload.success": + if e.complexity.CreatePasswordPayload.Success == nil { + break + } + + return e.complexity.CreatePasswordPayload.Success(childComplexity), true + case "CreatePersonalAPIKeyPayload.personalAPIKeyEdge": if e.complexity.CreatePersonalAPIKeyPayload.PersonalAPIKeyEdge == nil { break @@ -877,7 +888,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Identity.Profiles(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ProfileOrderBy)), true + return e.complexity.Identity.Profiles(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ProfileOrderBy), args["filter"].(*types.ProfileFilter)), true case "Identity.sessions": if e.complexity.Identity.Sessions == nil { break @@ -2357,6 +2368,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputForgotPasswordInput, ec.unmarshalInputInvitationOrder, ec.unmarshalInputInviteUserInput, + ec.unmarshalInputProfileFilter, ec.unmarshalInputProfileOrder, ec.unmarshalInputRegenerateSCIMTokenInput, ec.unmarshalInputRemoveUserInput, @@ -2614,6 +2626,7 @@ type Identity implements Node { last: Int before: CursorKey orderBy: ProfileOrder + filter: ProfileFilter ): ProfileConnection @goField(forceResolver: true) sessions( @@ -2985,6 +2998,11 @@ enum ProfileOrderField ) } +input ProfileFilter { + excludeContractEnded: Boolean + state: ProfileState +} + input ProfileOrder @goModel( model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy" @@ -3101,7 +3119,6 @@ input SignUpInput { input ActivateAccountInput { token: String! - password: String! } input ForgotPasswordInput { @@ -3267,6 +3284,10 @@ type ActivateAccountPayload { profile: Profile } +type CreatePasswordPayload { + success: Boolean! +} + type ForgotPasswordPayload { success: Boolean! } @@ -3557,6 +3578,11 @@ func (ec *executionContext) field_Identity_profiles_args(ctx context.Context, ra return nil, err } args["orderBy"] = arg4 + arg5, err := graphql.ProcessArgField(ctx, rawArgs, "filter", ec.unmarshalOProfileFilter2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileFilter) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } @@ -4730,6 +4756,35 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_ return fc, nil } +func (ec *executionContext) _CreatePasswordPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.CreatePasswordPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_CreatePasswordPayload_success, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_CreatePasswordPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreatePasswordPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _CreatePersonalAPIKeyPayload_personalAPIKeyEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreatePersonalAPIKeyPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -5377,7 +5432,7 @@ func (ec *executionContext) _Identity_profiles(ctx context.Context, field graphq ec.fieldContext_Identity_profiles, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Identity().Profiles(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.ProfileOrderBy)) + return ec.resolvers.Identity().Profiles(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.ProfileOrderBy), fc.Args["filter"].(*types.ProfileFilter)) }, nil, ec.marshalOProfileConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileConnection, @@ -15129,7 +15184,7 @@ func (ec *executionContext) unmarshalInputActivateAccountInput(ctx context.Conte asMap[k] = v } - fieldsInOrder := [...]string{"token", "password"} + fieldsInOrder := [...]string{"token"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -15143,13 +15198,6 @@ func (ec *executionContext) unmarshalInputActivateAccountInput(ctx context.Conte return it, err } it.Token = data - case "password": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Password = data } } @@ -15770,6 +15818,40 @@ func (ec *executionContext) unmarshalInputInviteUserInput(ctx context.Context, o return it, nil } +func (ec *executionContext) unmarshalInputProfileFilter(ctx context.Context, obj any) (types.ProfileFilter, error) { + var it types.ProfileFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"excludeContractEnded", "state"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "excludeContractEnded": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludeContractEnded")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ExcludeContractEnded = data + case "state": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state")) + data, err := ec.unmarshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState(ctx, v) + if err != nil { + return it, err + } + it.State = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputProfileOrder(ctx context.Context, obj any) (types.ProfileOrderBy, error) { var it types.ProfileOrderBy asMap := map[string]any{} @@ -16955,6 +17037,45 @@ func (ec *executionContext) _CreateOrganizationPayload(ctx context.Context, sel return out } +var createPasswordPayloadImplementors = []string{"CreatePasswordPayload"} + +func (ec *executionContext) _CreatePasswordPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreatePasswordPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, createPasswordPayloadImplementors) + + 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("CreatePasswordPayload") + case "success": + out.Values[i] = ec._CreatePasswordPayload_success(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 createPersonalAPIKeyPayloadImplementors = []string{"CreatePersonalAPIKeyPayload"} func (ec *executionContext) _CreatePersonalAPIKeyPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreatePersonalAPIKeyPayload) graphql.Marshaler { @@ -23298,6 +23419,14 @@ func (ec *executionContext) marshalOProfileConnection2ᚖgoᚗproboᚗincᚋprob return ec._ProfileConnection(ctx, sel, v) } +func (ec *executionContext) unmarshalOProfileFilter2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileFilter(ctx context.Context, v any) (*types.ProfileFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputProfileFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalOProfileOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileOrderBy(ctx context.Context, v any) (*types.ProfileOrderBy, error) { if v == nil { return nil, nil @@ -23306,6 +23435,36 @@ func (ec *executionContext) unmarshalOProfileOrder2ᚖgoᚗproboᚗincᚋprobo return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState(ctx context.Context, v any) (*coredata.ProfileState, error) { + if v == nil { + return nil, nil + } + tmp, err := graphql.UnmarshalString(v) + res := unmarshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState[tmp] + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState(ctx context.Context, sel ast.SelectionSet, v *coredata.ProfileState) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalString(marshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState[*v]) + return res +} + +var ( + unmarshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState = map[string]coredata.ProfileState{ + "ACTIVE": coredata.ProfileStateActive, + "INACTIVE": coredata.ProfileStateInactive, + } + marshalOProfileState2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐProfileState = map[coredata.ProfileState]string{ + coredata.ProfileStateActive: "ACTIVE", + coredata.ProfileStateInactive: "INACTIVE", + } +) + func (ec *executionContext) marshalORegenerateSCIMTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRegenerateSCIMTokenPayload(ctx context.Context, sel ast.SelectionSet, v *types.RegenerateSCIMTokenPayload) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/connect/v1/types/profile.go b/pkg/server/api/connect/v1/types/profile.go index c3d629dbd..476029e24 100644 --- a/pkg/server/api/connect/v1/types/profile.go +++ b/pkg/server/api/connect/v1/types/profile.go @@ -30,6 +30,7 @@ type ( Resolver any ParentID gid.GID + Filters *coredata.MembershipProfileFilter } ) @@ -37,6 +38,7 @@ func NewProfileConnection( p *page.Page[*coredata.MembershipProfile, coredata.MembershipProfileOrderField], resolver any, parentID gid.GID, + filters *coredata.MembershipProfileFilter, ) *ProfileConnection { edges := make([]*ProfileEdge, len(p.Data)) for i, profile := range p.Data { @@ -49,6 +51,7 @@ func NewProfileConnection( Resolver: resolver, ParentID: parentID, + Filters: filters, } } diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index 64212b9b8..d26fa8c91 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -35,8 +35,7 @@ type AcceptInvitationPayload struct { } type ActivateAccountInput struct { - Token string `json:"token"` - Password string `json:"password"` + Token string `json:"token"` } type ActivateAccountPayload struct { @@ -92,6 +91,10 @@ type CreateOrganizationPayload struct { Membership *Membership `json:"membership"` } +type CreatePasswordPayload struct { + Success bool `json:"success"` +} + type CreatePersonalAPIKeyInput struct { Name string `json:"name"` ExpiresAt time.Time `json:"expiresAt"` @@ -339,6 +342,11 @@ type ProfileEdge struct { Node *Profile `json:"node"` } +type ProfileFilter struct { + ExcludeContractEnded *bool `json:"excludeContractEnded,omitempty"` + State *coredata.ProfileState `json:"state,omitempty"` +} + type Query struct { } diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index ad35656be..c8f62ae26 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -33,17 +33,24 @@ func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector } // Profiles is the resolver for the profiles field. -func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) { +func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy, filter *types.ProfileFilter) (*types.ProfileConnection, error) { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList, authz.WithSkipAssumptionCheck()); err != nil { return nil, err } - filter := coredata.NewMembershipProfileFilter(nil) + filters := coredata.NewMembershipProfileFilter(nil) + if filter != nil { + filters = coredata.NewMembershipProfileFilter(filter.ExcludeContractEnded) + if filter.State != nil { + filters.WithState(*filter.State) + } + } if gqlutils.OnlyTotalCountSelected(ctx) { return &types.ProfileConnection{ Resolver: r, ParentID: obj.ID, + Filters: filters, }, nil } @@ -60,13 +67,13 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) - page, err := r.iam.AccountService.ListProfilesForIdentity(ctx, obj.ID, cursor, filter) + page, err := r.iam.AccountService.ListProfilesForIdentity(ctx, obj.ID, cursor, filters) if err != nil { r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err)) return nil, gqlutils.Internal(ctx) } - return types.NewProfileConnection(page, r, obj.ID), nil + return types.NewProfileConnection(page, r, obj.ID, filters), nil } // Sessions is the resolver for the sessions field. @@ -381,7 +388,6 @@ func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.Acti ctx, &iam.CreateIdentityFromInvitationRequest{ InvitationToken: input.Token, - Password: input.Password, }, ) if err != nil { @@ -565,19 +571,19 @@ func (r *mutationResolver) AssumeOrganizationSession(ctx context.Context, input childSession, membership, err := r.iam.SessionService.AssumeOrganizationSession(ctx, rootSession.ID, input.OrganizationID, input.Continue) if err != nil { var ( - errMembershipNotFound *iam.ErrMembershipNotFound - errPasswordRequired *iam.ErrPasswordRequired - errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired + errMembershipNotFound *iam.ErrMembershipNotFound + errPasswordAuthenticationRequired *iam.ErrPasswordAuthenticationRequired + errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired ) switch { case errors.As(err, &errMembershipNotFound): return nil, gqlutils.NotFound(ctx, err) - case errors.As(err, &errPasswordRequired): + case errors.As(err, &errPasswordAuthenticationRequired): return &types.AssumeOrganizationSessionPayload{ Result: types.PasswordRequired{ - Reason: types.ReauthenticationReason(errPasswordRequired.Reason), + Reason: types.ReauthenticationReason(errPasswordAuthenticationRequired.Reason), }, }, nil @@ -1199,7 +1205,7 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza return nil, gqlutils.Internal(ctx) } - return types.NewProfileConnection(page, r, obj.ID), nil + return types.NewProfileConnection(page, r, obj.ID, filter), nil } // SamlConfigurations is the resolver for the samlConfigurations field.