Implement activate account page

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-02-17 14:30:32 +04:00
parent 47aba96b69
commit 5ab5d2cc4c
13 changed files with 278 additions and 138 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<e920b2bbd380e12014a100bfb963c8f7>> * @generated SignedSource<<b79143becc90a4776f321d0e91db49bd>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,7 +10,7 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type ActivateAccountInput = { export type ActivateAccountInput = {
password: string; password?: string | null | undefined;
token: string; token: string;
}; };
export type ActivateAccountPageMutation$variables = { export type ActivateAccountPageMutation$variables = {

View File

@@ -1,14 +1,13 @@
import { formatError } from "@probo/helpers"; import { formatError, type GraphQLError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; 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 { useMutation } from "react-relay";
import { Link, useNavigate, useSearchParams } from "react-router"; import { Link, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod";
import type { ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql"; import type { ActivateAccountPageMutation } from "#/__generated__/iam/ActivateAccountPageMutation.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
const activateAccountMutation = graphql` const activateAccountMutation = graphql`
mutation ActivateAccountPageMutation( 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<typeof schema>;
export default function ActivateAccountPage() { export default function ActivateAccountPage() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const fullNameFromParams = searchParams.get("fullName") || ""; const submittedRef = useRef<boolean>(false);
usePageTitle(__("Sign up")); usePageTitle(__("Activate Account"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
password: "",
fullName: fullNameFromParams,
},
});
const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation); const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation);
const onSubmit = (data: FormData) => { const handleActivateAccount = useCallback((token: string) => {
const token = searchParams.get("token"); if (submittedRef.current) return;
if (!token) {
toast({
title: __("Signup failed"),
description: __("Invalid or missing invitation token"),
variant: "error",
});
return;
}
activateAccount({ activateAccount({
variables: { variables: {
input: { input: { token },
token,
password: data.password,
fullName: data.fullName,
},
}, },
onCompleted: (_, e) => { onCompleted: (_, errors: GraphQLError[] | null) => {
if (e) { if (errors) {
for (const err of errors) {
if (err.extensions?.code === "ALREADY_AUTHENTICATED") {
window.location.href = "/";
return;
}
}
toast({ toast({
title: __("Signup failed"), title: __("Activation failed"),
description: formatError(__("Signup failed"), e), description: formatError(__("Activation failed"), errors),
variant: "error", variant: "error",
}); });
return; return;
} }
toast({ toast({
title: __("Success"), title: __("Success"),
description: __( description: __(
"Account created successfully. Please accept your invitation to join the organization.", "Account activated successfully.",
), ),
variant: "success", variant: "success",
}); });
@@ -87,60 +67,37 @@ export default function ActivateAccountPage() {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Signup failed"), title: __("Activation failed"),
description: e.message, description: e.message,
variant: "error", 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 ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Create your account")}</h1> <h1 className="text-3xl font-bold">{__("Account Activation")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Set your password to join the organization")} {__("Activating your account…")}
</p> </p>
</div> </div>
<div className="text-center mt-6 text-sm text-txt-secondary">
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4"> <Link
<Field to="/auth/login"
label={__("Full Name")} className="underline hover:text-txt-primary"
type="text" >
placeholder={__("John Doe")} {__("Go back")}
{...register("fullName")} </Link>
required
error={formState.errors.fullName?.message}
/>
<Field
label={__("Password")}
type="password"
placeholder="••••••••"
{...register("password")}
required
error={formState.errors.password?.message}
/>
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
{formState.isLoading
? __("Creating account...")
: __("Create account")}
</Button>
</form>
<div className="text-center">
<p className="text-sm text-txt-tertiary">
{__("Already have an account?")}
{" "}
<Link
to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary"
>
{__("Log in here")}
</Link>
</p>
</div> </div>
</div> </div>
); );

View File

@@ -6,17 +6,17 @@ export const Invitation = () => {
return ( return (
<EmailLayout subject={'Invitation to join {{.OrganizationName}} on Probo'}> <EmailLayout subject={'Invitation to join {{.OrganizationName}} on Probo'}>
<Text style={bodyText}> <Text style={bodyText}>
You have been invited to join organization <strong>{'{{.OrganizationName}}'}</strong> on Probo. Click the button below to accept the invitation: You have been invited to join organization <strong>{'{{.OrganizationName}}'}</strong> on Probo. Click the button below to activate your account:
</Text> </Text>
<Section style={buttonContainer}> <Section style={buttonContainer}>
<Button style={button} href={'{{.InvitationUrl}}'}> <Button style={button} href={'{{.InvitationUrl}}'}>
Accept Invitation Activate Account
</Button> </Button>
</Section> </Section>
<Text style={footerText}> <Text style={footerText}>
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.
</Text> </Text>
</EmailLayout> </EmailLayout>
); );

View File

@@ -2,11 +2,11 @@ Probo
Hi {{.RecipientFullName}}, 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}} {{.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}} {{.SenderCompanyHeadquarterAddress}}
Powered By Probo Powered By Probo

View File

@@ -293,7 +293,6 @@ func (i *Invitations) ExpireByUserID(
expires_at = NOW() expires_at = NOW()
WHERE WHERE
user_id = @user_id user_id = @user_id
AND organization_id = @organization_id
AND %s AND %s
AND %s AND %s
` `

View File

@@ -47,7 +47,6 @@ type (
CreateIdentityFromInvitationRequest struct { CreateIdentityFromInvitationRequest struct {
InvitationToken string InvitationToken string
Password string
} }
LoadOrCreateIdentityRequest struct { LoadOrCreateIdentityRequest struct {
@@ -92,7 +91,6 @@ func (req CreateIdentityFromInvitationRequest) Validate() error {
v := validator.New() v := validator.New()
v.Check(req.InvitationToken, "invitationToken", validator.NotEmpty()) v.Check(req.InvitationToken, "invitationToken", validator.NotEmpty())
v.Check(req.Password, "password", PasswordValidator())
return v.Error() return v.Error()
} }
@@ -153,11 +151,6 @@ func (s *AuthService) ActivateAccount(
now = time.Now() 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( err = s.pg.WithTx(
ctx, ctx,
func(tx pg.Conn) error { func(tx pg.Conn) error {
@@ -197,7 +190,6 @@ func (s *AuthService) ActivateAccount(
return fmt.Errorf("cannot load identity: %w", err) return fmt.Errorf("cannot load identity: %w", err)
} }
identity.HashedPassword = hashedPassword
identity.EmailAddressVerified = true identity.EmailAddressVerified = true
identity.UpdatedAt = now identity.UpdatedAt = now

View File

@@ -326,15 +326,15 @@ func (e ErrInvitationNotDeleted) Error() string {
return fmt.Sprintf("cannot delete invitation %q in %q status", e.InvitationID, e.Status) return fmt.Sprintf("cannot delete invitation %q in %q status", e.InvitationID, e.Status)
} }
type ErrPasswordRequired struct { type ErrPasswordAuthenticationRequired struct {
Reason string Reason string
} }
func NewPasswordRequiredError(reason string) *ErrPasswordRequired { func NewPasswordAuthenticationRequiredError(reason string) *ErrPasswordAuthenticationRequired {
return &ErrPasswordRequired{Reason: reason} return &ErrPasswordAuthenticationRequired{Reason: reason}
} }
func (e *ErrPasswordRequired) Error() string { func (e *ErrPasswordAuthenticationRequired) Error() string {
return fmt.Sprintf("password authentication required: %s", e.Reason) return fmt.Sprintf("password authentication required: %s", e.Reason)
} }

View File

@@ -573,16 +573,23 @@ func (s SessionService) AssumeOrganizationSession(
return fmt.Errorf("cannot load SAML configuration: %w", err) return fmt.Errorf("cannot load SAML configuration: %w", err)
} }
if err == nil && samlConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired { if err == nil {
if rootSession.AuthMethod != coredata.AuthMethodSAML { switch samlConfig.EnforcementPolicy {
return NewSAMLAuthenticationRequiredError("policy_requirement") 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() tenantID := scope.GetTenantID()

View File

@@ -141,6 +141,7 @@ type Identity implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: ProfileOrder orderBy: ProfileOrder
filter: ProfileFilter
): ProfileConnection @goField(forceResolver: true) ): ProfileConnection @goField(forceResolver: true)
sessions( sessions(
@@ -512,6 +513,11 @@ enum ProfileOrderField
) )
} }
input ProfileFilter {
excludeContractEnded: Boolean
state: ProfileState
}
input ProfileOrder input ProfileOrder
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy" model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy"
@@ -628,7 +634,6 @@ input SignUpInput {
input ActivateAccountInput { input ActivateAccountInput {
token: String! token: String!
password: String!
} }
input ForgotPasswordInput { input ForgotPasswordInput {
@@ -794,6 +799,10 @@ type ActivateAccountPayload {
profile: Profile profile: Profile
} }
type CreatePasswordPayload {
success: Boolean!
}
type ForgotPasswordPayload { type ForgotPasswordPayload {
success: Boolean! success: Boolean!
} }

View File

@@ -108,6 +108,10 @@ type ComplexityRoot struct {
Organization func(childComplexity int) int Organization func(childComplexity int) int
} }
CreatePasswordPayload struct {
Success func(childComplexity int) int
}
CreatePersonalAPIKeyPayload struct { CreatePersonalAPIKeyPayload struct {
PersonalAPIKeyEdge func(childComplexity int) int PersonalAPIKeyEdge func(childComplexity int) int
Token func(childComplexity int) int Token func(childComplexity int) int
@@ -159,7 +163,7 @@ type ComplexityRoot struct {
ID func(childComplexity int) int ID func(childComplexity int) int
Permission func(childComplexity int, action string) int Permission func(childComplexity int, action string) int
PersonalAPIKeys func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) 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 Sessions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
} }
@@ -499,7 +503,7 @@ type ConnectorResolver interface {
Permission(ctx context.Context, obj *types.Connector, action string) (bool, error) Permission(ctx context.Context, obj *types.Connector, action string) (bool, error)
} }
type IdentityResolver interface { 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) 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) 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) 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 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": case "CreatePersonalAPIKeyPayload.personalAPIKeyEdge":
if e.complexity.CreatePersonalAPIKeyPayload.PersonalAPIKeyEdge == nil { if e.complexity.CreatePersonalAPIKeyPayload.PersonalAPIKeyEdge == nil {
break break
@@ -877,7 +888,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false 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": case "Identity.sessions":
if e.complexity.Identity.Sessions == nil { if e.complexity.Identity.Sessions == nil {
break break
@@ -2357,6 +2368,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputForgotPasswordInput, ec.unmarshalInputForgotPasswordInput,
ec.unmarshalInputInvitationOrder, ec.unmarshalInputInvitationOrder,
ec.unmarshalInputInviteUserInput, ec.unmarshalInputInviteUserInput,
ec.unmarshalInputProfileFilter,
ec.unmarshalInputProfileOrder, ec.unmarshalInputProfileOrder,
ec.unmarshalInputRegenerateSCIMTokenInput, ec.unmarshalInputRegenerateSCIMTokenInput,
ec.unmarshalInputRemoveUserInput, ec.unmarshalInputRemoveUserInput,
@@ -2614,6 +2626,7 @@ type Identity implements Node {
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: ProfileOrder orderBy: ProfileOrder
filter: ProfileFilter
): ProfileConnection @goField(forceResolver: true) ): ProfileConnection @goField(forceResolver: true)
sessions( sessions(
@@ -2985,6 +2998,11 @@ enum ProfileOrderField
) )
} }
input ProfileFilter {
excludeContractEnded: Boolean
state: ProfileState
}
input ProfileOrder input ProfileOrder
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy" model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.ProfileOrderBy"
@@ -3101,7 +3119,6 @@ input SignUpInput {
input ActivateAccountInput { input ActivateAccountInput {
token: String! token: String!
password: String!
} }
input ForgotPasswordInput { input ForgotPasswordInput {
@@ -3267,6 +3284,10 @@ type ActivateAccountPayload {
profile: Profile profile: Profile
} }
type CreatePasswordPayload {
success: Boolean!
}
type ForgotPasswordPayload { type ForgotPasswordPayload {
success: Boolean! success: Boolean!
} }
@@ -3557,6 +3578,11 @@ func (ec *executionContext) field_Identity_profiles_args(ctx context.Context, ra
return nil, err return nil, err
} }
args["orderBy"] = arg4 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 return args, nil
} }
@@ -4730,6 +4756,35 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_membership(_
return fc, nil 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) { func (ec *executionContext) _CreatePersonalAPIKeyPayload_personalAPIKeyEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreatePersonalAPIKeyPayload) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -5377,7 +5432,7 @@ func (ec *executionContext) _Identity_profiles(ctx context.Context, field graphq
ec.fieldContext_Identity_profiles, ec.fieldContext_Identity_profiles,
func(ctx context.Context) (any, error) { func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx) 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, nil,
ec.marshalOProfileConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐProfileConnection, 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 asMap[k] = v
} }
fieldsInOrder := [...]string{"token", "password"} fieldsInOrder := [...]string{"token"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -15143,13 +15198,6 @@ func (ec *executionContext) unmarshalInputActivateAccountInput(ctx context.Conte
return it, err return it, err
} }
it.Token = data 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 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) { func (ec *executionContext) unmarshalInputProfileOrder(ctx context.Context, obj any) (types.ProfileOrderBy, error) {
var it types.ProfileOrderBy var it types.ProfileOrderBy
asMap := map[string]any{} asMap := map[string]any{}
@@ -16955,6 +17037,45 @@ func (ec *executionContext) _CreateOrganizationPayload(ctx context.Context, sel
return out 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"} var createPersonalAPIKeyPayloadImplementors = []string{"CreatePersonalAPIKeyPayload"}
func (ec *executionContext) _CreatePersonalAPIKeyPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreatePersonalAPIKeyPayload) graphql.Marshaler { 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) 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) { 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 { if v == nil {
return nil, nil return nil, nil
@@ -23306,6 +23435,36 @@ func (ec *executionContext) unmarshalOProfileOrder2ᚖgoᚗproboᚗincᚋprobo
return &res, graphql.ErrorOnPath(ctx, err) 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 { 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 { if v == nil {
return graphql.Null return graphql.Null

View File

@@ -30,6 +30,7 @@ type (
Resolver any Resolver any
ParentID gid.GID ParentID gid.GID
Filters *coredata.MembershipProfileFilter
} }
) )
@@ -37,6 +38,7 @@ func NewProfileConnection(
p *page.Page[*coredata.MembershipProfile, coredata.MembershipProfileOrderField], p *page.Page[*coredata.MembershipProfile, coredata.MembershipProfileOrderField],
resolver any, resolver any,
parentID gid.GID, parentID gid.GID,
filters *coredata.MembershipProfileFilter,
) *ProfileConnection { ) *ProfileConnection {
edges := make([]*ProfileEdge, len(p.Data)) edges := make([]*ProfileEdge, len(p.Data))
for i, profile := range p.Data { for i, profile := range p.Data {
@@ -49,6 +51,7 @@ func NewProfileConnection(
Resolver: resolver, Resolver: resolver,
ParentID: parentID, ParentID: parentID,
Filters: filters,
} }
} }

View File

@@ -35,8 +35,7 @@ type AcceptInvitationPayload struct {
} }
type ActivateAccountInput struct { type ActivateAccountInput struct {
Token string `json:"token"` Token string `json:"token"`
Password string `json:"password"`
} }
type ActivateAccountPayload struct { type ActivateAccountPayload struct {
@@ -92,6 +91,10 @@ type CreateOrganizationPayload struct {
Membership *Membership `json:"membership"` Membership *Membership `json:"membership"`
} }
type CreatePasswordPayload struct {
Success bool `json:"success"`
}
type CreatePersonalAPIKeyInput struct { type CreatePersonalAPIKeyInput struct {
Name string `json:"name"` Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt"` ExpiresAt time.Time `json:"expiresAt"`
@@ -339,6 +342,11 @@ type ProfileEdge struct {
Node *Profile `json:"node"` Node *Profile `json:"node"`
} }
type ProfileFilter struct {
ExcludeContractEnded *bool `json:"excludeContractEnded,omitempty"`
State *coredata.ProfileState `json:"state,omitempty"`
}
type Query struct { type Query struct {
} }

View File

@@ -33,17 +33,24 @@ func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector
} }
// Profiles is the resolver for the profiles field. // 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 { if err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList, authz.WithSkipAssumptionCheck()); err != nil {
return nil, err 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) { if gqlutils.OnlyTotalCountSelected(ctx) {
return &types.ProfileConnection{ return &types.ProfileConnection{
Resolver: r, Resolver: r,
ParentID: obj.ID, ParentID: obj.ID,
Filters: filters,
}, nil }, nil
} }
@@ -60,13 +67,13 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy) 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 { if err != nil {
r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err)) r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err))
return nil, gqlutils.Internal(ctx) 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. // Sessions is the resolver for the sessions field.
@@ -381,7 +388,6 @@ func (r *mutationResolver) ActivateAccount(ctx context.Context, input types.Acti
ctx, ctx,
&iam.CreateIdentityFromInvitationRequest{ &iam.CreateIdentityFromInvitationRequest{
InvitationToken: input.Token, InvitationToken: input.Token,
Password: input.Password,
}, },
) )
if err != nil { 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) childSession, membership, err := r.iam.SessionService.AssumeOrganizationSession(ctx, rootSession.ID, input.OrganizationID, input.Continue)
if err != nil { if err != nil {
var ( var (
errMembershipNotFound *iam.ErrMembershipNotFound errMembershipNotFound *iam.ErrMembershipNotFound
errPasswordRequired *iam.ErrPasswordRequired errPasswordAuthenticationRequired *iam.ErrPasswordAuthenticationRequired
errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired
) )
switch { switch {
case errors.As(err, &errMembershipNotFound): case errors.As(err, &errMembershipNotFound):
return nil, gqlutils.NotFound(ctx, err) return nil, gqlutils.NotFound(ctx, err)
case errors.As(err, &errPasswordRequired): case errors.As(err, &errPasswordAuthenticationRequired):
return &types.AssumeOrganizationSessionPayload{ return &types.AssumeOrganizationSessionPayload{
Result: types.PasswordRequired{ Result: types.PasswordRequired{
Reason: types.ReauthenticationReason(errPasswordRequired.Reason), Reason: types.ReauthenticationReason(errPasswordAuthenticationRequired.Reason),
}, },
}, nil }, nil
@@ -1199,7 +1205,7 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
return nil, gqlutils.Internal(ctx) 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. // SamlConfigurations is the resolver for the samlConfigurations field.