feat: return field name in generated node text label

GitOrigin-RevId: c37d048759b18337eafafbf9cdf8449253b64836
This commit is contained in:
Jonas Hungershausen 2025-09-30 08:04:01 -04:00 committed by ory-bot
parent e26aab102f
commit 8c7a3dc5eb
7 changed files with 57 additions and 86 deletions

View File

@ -4,6 +4,5 @@
"github.com/ory/x","Apache-2.0"
"github.com/stretchr/testify","MIT"
"go.opentelemetry.io/otel/sdk","Apache-2.0"
"go.opentelemetry.io/otel/sdk","BSD-3-Clause"
"golang.org/x/text","BSD-3-Clause"

1 module name licenses
4 github.com/stretchr/testify MIT
5 go.opentelemetry.io/otel/sdk Apache-2.0
6 go.opentelemetry.io/otel/sdk golang.org/x/text BSD-3-Clause
golang.org/x/text BSD-3-Clause
7
8

View File

@ -48,7 +48,7 @@ func init() {
"NewInfoNodeLabelRecoveryCode": text.NewInfoNodeLabelRecoveryCode(),
"NewInfoNodeInputPassword": text.NewInfoNodeInputPassword(),
"NewInfoNodeInputPhoneNumber": text.NewInfoNodeInputPhoneNumber(),
"NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}"),
"NewInfoNodeLabelGenerated": text.NewInfoNodeLabelGenerated("{title}", "{name}"),
"NewInfoNodeLabelSave": text.NewInfoNodeLabelSave(),
"NewInfoNodeLabelSubmit": text.NewInfoNodeLabelSubmit(),
"NewInfoNodeLabelID": text.NewInfoNodeLabelID(),

View File

@ -6,36 +6,15 @@ package login
import (
"context"
"github.com/pkg/errors"
"github.com/samber/lo"
"github.com/ory/herodot"
"github.com/ory/kratos/text"
"github.com/ory/x/jsonschemax"
"github.com/ory/jsonschema/v3"
"github.com/ory/kratos/schema"
)
type identifierLabelExtension struct {
field string
identifierLabelCandidates []string
}
var (
_ schema.CompileExtension = new(identifierLabelExtension)
ErrUnknownTrait = herodot.ErrInternalServerError.WithReasonf("Trait does not exist in identity schema")
)
func GetIdentifierLabelFromSchema(ctx context.Context, schemaURL string) (*text.Message, error) {
return GetIdentifierLabelFromSchemaWithField(ctx, schemaURL, "")
}
func GetIdentifierLabelFromSchemaWithField(ctx context.Context, schemaURL string, trait string) (*text.Message, error) {
ext := &identifierLabelExtension{
field: trait,
}
runner, err := schema.NewExtensionRunner(ctx, schema.WithCompileRunners(ext))
runner, err := schema.NewExtensionRunner(ctx)
if err != nil {
return nil, err
}
@ -43,50 +22,31 @@ func GetIdentifierLabelFromSchemaWithField(ctx context.Context, schemaURL string
c.ExtractAnnotations = true
runner.Register(c)
s, err := c.Compile(ctx, schemaURL)
paths, err := jsonschemax.ListPaths(ctx, schemaURL, c)
if err != nil {
return nil, err
}
if trait != "" {
f, ok := s.Properties["traits"].Properties[trait]
if !ok {
knownTraits := lo.Keys(s.Properties["traits"].Properties)
return nil, errors.WithStack(ErrUnknownTrait.WithDetail("trait", trait).WithDetail("known_traits", knownTraits))
}
return text.NewInfoNodeLabelGenerated(f.Title), nil
}
metaLabel := text.NewInfoNodeLabelID()
if label := ext.getLabel(); label != "" {
metaLabel = text.NewInfoNodeLabelGenerated(label)
}
return metaLabel, nil
}
func (i *identifierLabelExtension) Run(_ jsonschema.CompilerContext, config schema.ExtensionConfig, rawSchema map[string]interface{}) error {
if config.Credentials.Password.Identifier ||
config.Credentials.WebAuthn.Identifier ||
config.Credentials.Passkey.DisplayName ||
config.Credentials.TOTP.AccountName ||
config.Credentials.Code.Identifier {
if title, ok := rawSchema["title"]; ok {
// The jsonschema compiler validates the title to be a string, so this should always work.
switch t := title.(type) {
case string:
if t != "" {
i.identifierLabelCandidates = append(i.identifierLabelCandidates, t)
}
labels := []jsonschemax.Path{}
for _, path := range paths {
if ext := path.CustomProperties[schema.ExtensionName]; ext != nil {
config, ok := ext.(*schema.ExtensionConfig)
if !ok {
continue
}
if config.Credentials.Password.Identifier ||
config.Credentials.WebAuthn.Identifier ||
config.Credentials.Passkey.DisplayName ||
config.Credentials.TOTP.AccountName ||
config.Credentials.Code.Identifier {
labels = append(labels, path)
}
}
}
return nil
}
func (i *identifierLabelExtension) getLabel() string {
if len(i.identifierLabelCandidates) != 1 {
// sane default is set elsewhere
return ""
metaLabel := text.NewInfoNodeLabelID()
if len(labels) == 1 && labels[0].Title != "" {
metaLabel = text.NewInfoNodeLabelGenerated(labels[0].Title, labels[0].Name)
}
return i.identifierLabelCandidates[0]
return metaLabel, nil
}

View File

@ -4,7 +4,6 @@
package login
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
@ -52,28 +51,32 @@ func constructSchema(t *testing.T, ecModifier, ucModifier func(*schema.Extension
uc, err = sjson.DeleteBytes(uc, "organizations.matcher")
require.NoError(t, err)
return "base64://" + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`
return "base64://" + base64.StdEncoding.EncodeToString(fmt.Appendf(nil, `
{
"$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"title": "Email",
"type": "string",
"ory.sh/kratos": %s
},
"username": {
"title": "Username",
"type": "string",
"ory.sh/kratos": %s
}
}
}
}
}`, ec, uc)))
}`, ec, uc))
}
func TestGetIdentifierLabelFromSchema(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct {
name string
emailConfig, usernameConfig func(*schema.ExtensionConfig)
@ -84,35 +87,35 @@ func TestGetIdentifierLabelFromSchema(t *testing.T) {
emailConfig: func(c *schema.ExtensionConfig) {
c.Credentials.Password.Identifier = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "email for webauthn",
emailConfig: func(c *schema.ExtensionConfig) {
c.Credentials.WebAuthn.Identifier = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "email for totp",
emailConfig: func(c *schema.ExtensionConfig) {
c.Credentials.TOTP.AccountName = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "email for code",
emailConfig: func(c *schema.ExtensionConfig) {
c.Credentials.Code.Identifier = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "email for passkey",
emailConfig: func(c *schema.ExtensionConfig) {
c.Credentials.Passkey.DisplayName = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "email for all",
@ -122,14 +125,14 @@ func TestGetIdentifierLabelFromSchema(t *testing.T) {
c.Credentials.TOTP.AccountName = true
c.Credentials.Code.Identifier = true
},
expected: text.NewInfoNodeLabelGenerated("Email"),
expected: text.NewInfoNodeLabelGenerated("Email", "traits.email"),
},
{
name: "username works as well",
usernameConfig: func(c *schema.ExtensionConfig) {
c.Credentials.Password.Identifier = true
},
expected: text.NewInfoNodeLabelGenerated("Username"),
expected: text.NewInfoNodeLabelGenerated("Username", "traits.username"),
},
{
name: "multiple identifiers",
@ -147,7 +150,7 @@ func TestGetIdentifierLabelFromSchema(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
label, err := GetIdentifierLabelFromSchema(ctx, constructSchema(t, tc.emailConfig, tc.usernameConfig))
label, err := GetIdentifierLabelFromSchema(t.Context(), constructSchema(t, tc.emailConfig, tc.usernameConfig))
require.NoError(t, err)
assert.Equal(t, tc.expected, label)
})

View File

@ -682,7 +682,7 @@ func TestRegistration(t *testing.T) {
node.NewCSRFNode(nosurfx.FakeCSRFToken),
node.NewInputField("traits.email", nil, node.DefaultGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
a.Autocomplete = node.InputAttributeAutocompleteEmail
})).WithMetaLabel(text.NewInfoNodeLabelGenerated("E-Mail")),
})).WithMetaLabel(text.NewInfoNodeLabelGenerated("E-Mail", "traits.email")),
node.NewInputField("password", nil, node.PasswordGroup, node.InputAttributeTypePassword, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
a.Autocomplete = node.InputAttributeAutocompleteNewPassword
})).WithMetaLabel(text.NewInfoNodeInputPassword()),

View File

@ -51,13 +51,14 @@ func NewInfoNodeInputPassword() *Message {
}
}
func NewInfoNodeLabelGenerated(title string) *Message {
func NewInfoNodeLabelGenerated(title string, name string) *Message {
return &Message{
ID: InfoNodeLabelGenerated,
Text: title,
Type: Info,
Context: context(map[string]any{
"title": title,
"name": name,
}),
}
}

View File

@ -32,8 +32,10 @@ func toFormType(n string, i interface{}) UiNodeInputAttributeType {
return InputAttributeTypeText
}
type InputAttributesModifier func(attributes *InputAttributes)
type InputAttributesModifiers []InputAttributesModifier
type (
InputAttributesModifier func(attributes *InputAttributes)
InputAttributesModifiers []InputAttributesModifier
)
func WithRequiredInputAttribute(a *InputAttributes) {
a.Required = true
@ -58,8 +60,10 @@ func applyInputAttributes(opts []InputAttributesModifier, attributes *InputAttri
return attributes
}
type ImageAttributesModifier func(attributes *ImageAttributes)
type ImageAttributesModifiers []ImageAttributesModifier
type (
ImageAttributesModifier func(attributes *ImageAttributes)
ImageAttributesModifiers []ImageAttributesModifier
)
func WithImageAttributes(f func(a *ImageAttributes)) func(a *ImageAttributes) {
return func(a *ImageAttributes) {
@ -74,8 +78,10 @@ func applyImageAttributes(opts ImageAttributesModifiers, attributes *ImageAttrib
return attributes
}
type ScriptAttributesModifier func(attributes *ScriptAttributes)
type ScriptAttributesModifiers []ScriptAttributesModifier
type (
ScriptAttributesModifier func(attributes *ScriptAttributes)
ScriptAttributesModifiers []ScriptAttributesModifier
)
func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAttributes) *ScriptAttributes {
for _, f := range opts {
@ -84,8 +90,10 @@ func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAtt
return attributes
}
type DivisionAttributesModifier func(attributes *DivisionAttributes)
type DivisionAttributesModifiers []DivisionAttributesModifier
type (
DivisionAttributesModifier func(attributes *DivisionAttributes)
DivisionAttributesModifiers []DivisionAttributesModifier
)
func WithDivisionAttributes(f func(a *DivisionAttributes)) func(a *DivisionAttributes) {
return func(a *DivisionAttributes) {
@ -212,7 +220,7 @@ func NewInputFieldFromSchema(name string, group UiNodeGroup, p jsonschemax.Path,
var meta Meta
if len(p.Title) > 0 {
meta.Label = text.NewInfoNodeLabelGenerated(p.Title)
meta.Label = text.NewInfoNodeLabelGenerated(p.Title, name)
}
return &Node{