Split inactive profile state

Replace the binary profile ACTIVE/INACTIVE model with PENDING, ACTIVE,
and DEACTIVATED so invited-but-not-yet-activated members remain
assignable to assets, data, and risks instead of being treated like
deactivated users.

Add activated_at/deactivated_at timestamps and Mark* lifecycle helpers,
and update every transition (create, invite/re-invite, activation,
archive, SCIM, SAML, sessions, compliance-portal grant) to the new
states. Expose a multi-state states[] filter across coredata, GraphQL,
MCP, and the console owner pickers, which now request ACTIVE and
PENDING members.

A migration renames the membership_state enum, classifies existing
inactive profiles as PENDING from recent invitation activity, and
backfills the new timestamp columns.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-28 17:03:52 +02:00
parent 6b3913b189
commit 4a276e3ef7
29 changed files with 261 additions and 64 deletions

View File

@@ -30,9 +30,10 @@ type Profile implements Node {
enum ProfileState
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileState") {
PENDING @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStatePending")
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
DEACTIVATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateDeactivated")
}
enum ProfileSource
@@ -71,6 +72,7 @@ enum ProfileOrderField
input ProfileFilter {
contractEnded: Boolean
state: ProfileState
states: [ProfileState!]
query: String
role: MembershipRole
kind: String

View File

@@ -32,6 +32,10 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
if filter != nil {
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
if len(filter.States) > 0 {
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}

View File

@@ -188,6 +188,10 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
if filter != nil {
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
if len(filter.States) > 0 {
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}

View File

@@ -71,7 +71,7 @@ func (r *mutationResolver) DeactivateUser(ctx context.Context, input types.Deact
_, err := r.iam.OrganizationService.UpdateUserState(
ctx,
input.ProfileID,
coredata.ProfileStateInactive,
coredata.ProfileStateDeactivated,
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot deactivate profile", log.Error(err))

View File

@@ -9,9 +9,10 @@ type OrganizationContext {
enum ProfileState
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProfileState") {
PENDING @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStatePending")
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateActive")
INACTIVE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
DEACTIVATED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateDeactivated")
}
enum MembershipRole
@@ -69,6 +70,7 @@ input ProfileOrder
input ProfileFilter {
contractEnded: Boolean
state: ProfileState
states: [ProfileState!]
query: String
role: MembershipRole
kind: String

View File

@@ -119,6 +119,10 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
if filter != nil {
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
if len(filter.States) > 0 {
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}

View File

@@ -2776,6 +2776,10 @@ func (r *Resolver) ListUsersTool(ctx context.Context, req *mcp.CallToolRequest,
if input.Filter != nil {
filter = coredata.NewMembershipProfileFilter(input.Filter.ContractEnded).WithMembership()
if len(input.Filter.States) > 0 {
filter.WithStates(input.Filter.States...)
}
if input.Filter.State != nil {
filter.WithState(*input.Filter.State)
}

View File

@@ -184,8 +184,9 @@ components:
ProfileState:
type: string
enum:
- PENDING
- ACTIVE
- INACTIVE
- DEACTIVATED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ProfileState
ProfileKind:
@@ -526,7 +527,7 @@ components:
description: Profile source (MANUAL, SCIM, or SAML)
state:
$ref: "#/components/schemas/ProfileState"
description: Profile state (ACTIVE or INACTIVE)
description: Profile state (PENDING, ACTIVE, or DEACTIVATED)
position:
type:
- string
@@ -578,7 +579,12 @@ components:
description: Filter by contract status. True returns only users with ended contracts, false returns only users with active or no contract.
state:
$ref: "#/components/schemas/ProfileState"
description: Filter by profile state (ACTIVE or INACTIVE)
description: Filter by profile state (PENDING, ACTIVE, or DEACTIVATED)
states:
type: array
items:
$ref: "#/components/schemas/ProfileState"
description: Filter by profile states (PENDING, ACTIVE, or DEACTIVATED)
query:
type: string
description: Search by full name, email address, or position