mirror of https://github.com/ory/kratos
feat: return field name in generated node text label
GitOrigin-RevId: c37d048759b18337eafafbf9cdf8449253b64836
This commit is contained in:
parent
e26aab102f
commit
8c7a3dc5eb
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
Loading…
Reference in New Issue