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
* @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 = {

View File

@@ -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<typeof schema>;
export default function ActivateAccountPage() {
const { __ } = useTranslate();
const { toast } = useToast();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const fullNameFromParams = searchParams.get("fullName") || "";
const submittedRef = useRef<boolean>(false);
usePageTitle(__("Sign up"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: {
password: "",
fullName: fullNameFromParams,
},
});
usePageTitle(__("Activate Account"));
const [activateAccount] = useMutation<ActivateAccountPageMutation>(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 (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<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">
{__("Set your password to join the organization")}
{__("Activating your account…")}
</p>
</div>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field
label={__("Full Name")}
type="text"
placeholder={__("John Doe")}
{...register("fullName")}
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 className="text-center mt-6 text-sm text-txt-secondary">
<Link
to="/auth/login"
className="underline hover:text-txt-primary"
>
{__("Go back")}
</Link>
</div>
</div>
);

View File

@@ -6,17 +6,17 @@ export const Invitation = () => {
return (
<EmailLayout subject={'Invitation to join {{.OrganizationName}} on Probo'}>
<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>
<Section style={buttonContainer}>
<Button style={button} href={'{{.InvitationUrl}}'}>
Accept Invitation
Activate Account
</Button>
</Section>
<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>
</EmailLayout>
);

View File

@@ -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

View File

@@ -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
`

View File

@@ -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

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)
}
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)
}

View File

@@ -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()

View File

@@ -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!
}

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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 {
}

View File

@@ -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.