snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
12
ui/node/.snapshots/TestNodeMarshalJSON-anchor_node.json
Normal file
12
ui/node/.snapshots/TestNodeMarshalJSON-anchor_node.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "a",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"href": "https://example.com",
|
||||
"title": null,
|
||||
"id": "",
|
||||
"node_type": "a"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
10
ui/node/.snapshots/TestNodeMarshalJSON-division_node.json
Normal file
10
ui/node/.snapshots/TestNodeMarshalJSON-division_node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"type": "div",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"id": "",
|
||||
"node_type": "div"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "email",
|
||||
"type": "",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
13
ui/node/.snapshots/TestNodeMarshalJSON-image_node.json
Normal file
13
ui/node/.snapshots/TestNodeMarshalJSON-image_node.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "img",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"src": "image.jpg",
|
||||
"id": "",
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"node_type": "img"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
13
ui/node/.snapshots/TestNodeMarshalJSON-input_node.json
Normal file
13
ui/node/.snapshots/TestNodeMarshalJSON-input_node.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"value": "secret",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
17
ui/node/.snapshots/TestNodeMarshalJSON-script_node.json
Normal file
17
ui/node/.snapshots/TestNodeMarshalJSON-script_node.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "script",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"src": "script.js",
|
||||
"async": false,
|
||||
"referrerpolicy": "",
|
||||
"crossorigin": "",
|
||||
"integrity": "",
|
||||
"type": "",
|
||||
"id": "",
|
||||
"nonce": "",
|
||||
"node_type": "script"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
11
ui/node/.snapshots/TestNodeMarshalJSON-text_node.json
Normal file
11
ui/node/.snapshots/TestNodeMarshalJSON-text_node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "text",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"text": null,
|
||||
"id": "",
|
||||
"node_type": "text"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
542
ui/node/attributes.go
Normal file
542
ui/node/attributes.go
Normal file
@@ -0,0 +1,542 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/x/webauthnx/js"
|
||||
)
|
||||
|
||||
const (
|
||||
InputAttributeTypeText UiNodeInputAttributeType = "text"
|
||||
InputAttributeTypePassword UiNodeInputAttributeType = "password"
|
||||
InputAttributeTypeNumber UiNodeInputAttributeType = "number"
|
||||
InputAttributeTypeCheckbox UiNodeInputAttributeType = "checkbox"
|
||||
InputAttributeTypeHidden UiNodeInputAttributeType = "hidden"
|
||||
InputAttributeTypeEmail UiNodeInputAttributeType = "email"
|
||||
InputAttributeTypeTel UiNodeInputAttributeType = "tel"
|
||||
InputAttributeTypeSubmit UiNodeInputAttributeType = "submit"
|
||||
InputAttributeTypeButton UiNodeInputAttributeType = "button"
|
||||
InputAttributeTypeDateTimeLocal UiNodeInputAttributeType = "datetime-local"
|
||||
InputAttributeTypeDate UiNodeInputAttributeType = "date"
|
||||
InputAttributeTypeURI UiNodeInputAttributeType = "url"
|
||||
)
|
||||
|
||||
const (
|
||||
InputAttributeAutocompleteEmail UiNodeInputAttributeAutocomplete = "email"
|
||||
InputAttributeAutocompleteTel UiNodeInputAttributeAutocomplete = "tel"
|
||||
InputAttributeAutocompleteUrl UiNodeInputAttributeAutocomplete = "url"
|
||||
InputAttributeAutocompleteCurrentPassword UiNodeInputAttributeAutocomplete = "current-password"
|
||||
InputAttributeAutocompleteNewPassword UiNodeInputAttributeAutocomplete = "new-password"
|
||||
InputAttributeAutocompleteOneTimeCode UiNodeInputAttributeAutocomplete = "one-time-code"
|
||||
InputAttributeAutocompleteUsername UiNodeInputAttributeAutocomplete = "username"
|
||||
InputAttributeAutocompleteUsernameWebauthn UiNodeInputAttributeAutocomplete = "username webauthn"
|
||||
)
|
||||
|
||||
// swagger:enum UiNodeInputAttributeType
|
||||
type UiNodeInputAttributeType string
|
||||
|
||||
// swagger:enum UiNodeInputAttributeAutocomplete
|
||||
type UiNodeInputAttributeAutocomplete string
|
||||
|
||||
// Attributes represents a list of attributes (e.g. `href="foo"` for links).
|
||||
//
|
||||
// swagger:model uiNodeAttributes
|
||||
type Attributes interface {
|
||||
// swagger:ignore
|
||||
ID() string
|
||||
|
||||
// swagger:ignore
|
||||
Reset()
|
||||
|
||||
// swagger:ignore
|
||||
SetValue(value interface{})
|
||||
|
||||
// swagger:ignore
|
||||
GetValue() interface{}
|
||||
|
||||
// swagger:ignore
|
||||
GetNodeType() UiNodeType
|
||||
|
||||
// swagger:ignore
|
||||
Matches(other Attributes) bool
|
||||
}
|
||||
|
||||
// InputAttributes represents the attributes of an input node
|
||||
//
|
||||
// swagger:model uiNodeInputAttributes
|
||||
type InputAttributes struct {
|
||||
// The input's element name.
|
||||
//
|
||||
// required: true
|
||||
Name string `json:"name"`
|
||||
|
||||
// The input's element type.
|
||||
//
|
||||
// required: true
|
||||
Type UiNodeInputAttributeType `json:"type" faker:"-"`
|
||||
|
||||
// The input's value.
|
||||
FieldValue interface{} `json:"value,omitempty" faker:"string"`
|
||||
|
||||
// Mark this input field as required.
|
||||
Required bool `json:"required,omitempty"`
|
||||
|
||||
// The autocomplete attribute for the input.
|
||||
Autocomplete UiNodeInputAttributeAutocomplete `json:"autocomplete,omitempty"`
|
||||
|
||||
// The input's label text.
|
||||
Label *text.Message `json:"label,omitempty"`
|
||||
|
||||
// The input's pattern.
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
|
||||
// Sets the input's disabled field to true or false.
|
||||
//
|
||||
// required: true
|
||||
Disabled bool `json:"disabled"`
|
||||
|
||||
// OnClick may contain javascript which should be executed on click. This is primarily
|
||||
// used for WebAuthn.
|
||||
//
|
||||
// Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
|
||||
OnClick string `json:"onclick,omitempty"`
|
||||
|
||||
// OnClickTrigger may contain a WebAuthn trigger which should be executed on click.
|
||||
//
|
||||
// The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
|
||||
OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"`
|
||||
|
||||
// OnLoad may contain javascript which should be executed on load. This is primarily
|
||||
// used for WebAuthn.
|
||||
//
|
||||
// Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
|
||||
OnLoad string `json:"onload,omitempty"`
|
||||
|
||||
// OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.
|
||||
//
|
||||
// The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
|
||||
OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"`
|
||||
|
||||
// MaxLength may contain the input's maximum length.
|
||||
MaxLength int `json:"maxlength,omitempty"`
|
||||
|
||||
// The allowed values for the input when the underlying JSON schema
|
||||
// defines an `enum`. When present, clients should render the field as a
|
||||
// select/dropdown rather than a free-form text input. When absent,
|
||||
// clients continue to render a plain text input, so this field is
|
||||
// backward compatible with UIs that do not look at it.
|
||||
Options []InputAttributesOption `json:"options,omitempty" faker:"-"`
|
||||
|
||||
// NodeType represents this node's types. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
// Represents a single selectable value for an input whose JSON schema
|
||||
// defined an `enum`. The value is always a scalar JSON type (string, number,
|
||||
// or boolean) serialized verbatim from the schema. When present, clients
|
||||
// should render the parent input as a select/dropdown.
|
||||
//
|
||||
// swagger:model uiNodeInputAttributesOption
|
||||
type InputAttributesOption struct {
|
||||
// The value that will be submitted when this option is picked.
|
||||
// It is serialized verbatim from the JSON schema `enum` entry, so it is
|
||||
// always a scalar JSON value (string, number, or boolean).
|
||||
//
|
||||
// required: true
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
// ImageAttributes represents the attributes of an image node.
|
||||
//
|
||||
// swagger:model uiNodeImageAttributes
|
||||
type ImageAttributes struct {
|
||||
// The image's source URL.
|
||||
//
|
||||
// format: uri
|
||||
// required: true
|
||||
Source string `json:"src"`
|
||||
|
||||
// A unique identifier
|
||||
//
|
||||
// required: true
|
||||
Identifier string `json:"id"`
|
||||
|
||||
// Width of the image
|
||||
//
|
||||
// required: true
|
||||
Width int `json:"width"`
|
||||
|
||||
// Height of the image
|
||||
//
|
||||
// required: true
|
||||
Height int `json:"height"`
|
||||
|
||||
// NodeType represents this node's types. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "img".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
// AnchorAttributes represents the attributes of an anchor node.
|
||||
//
|
||||
// swagger:model uiNodeAnchorAttributes
|
||||
type AnchorAttributes struct {
|
||||
// The link's href (destination) URL.
|
||||
//
|
||||
// format: uri
|
||||
// required: true
|
||||
HREF string `json:"href"`
|
||||
|
||||
// The link's title.
|
||||
//
|
||||
// required: true
|
||||
Title *text.Message `json:"title"`
|
||||
|
||||
// A unique identifier
|
||||
//
|
||||
// required: true
|
||||
Identifier string `json:"id"`
|
||||
|
||||
// NodeType represents this node's types. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "a".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
// TextAttributes represents the attributes of a text node.
|
||||
//
|
||||
// swagger:model uiNodeTextAttributes
|
||||
type TextAttributes struct {
|
||||
// The text of the text node.
|
||||
//
|
||||
// required: true
|
||||
Text *text.Message `json:"text"`
|
||||
|
||||
// A unique identifier
|
||||
//
|
||||
// required: true
|
||||
Identifier string `json:"id"`
|
||||
|
||||
// NodeType represents this node's types. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "text".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
// ScriptAttributes represent script nodes which load javascript.
|
||||
//
|
||||
// swagger:model uiNodeScriptAttributes
|
||||
type ScriptAttributes struct {
|
||||
// The script source
|
||||
//
|
||||
// required: true
|
||||
Source string `json:"src"`
|
||||
|
||||
// The script async type
|
||||
//
|
||||
// required: true
|
||||
Async bool `json:"async"`
|
||||
|
||||
// The script referrer policy
|
||||
//
|
||||
// required: true
|
||||
ReferrerPolicy string `json:"referrerpolicy"`
|
||||
|
||||
// The script cross origin policy
|
||||
//
|
||||
// required: true
|
||||
CrossOrigin string `json:"crossorigin"`
|
||||
|
||||
// The script's integrity hash
|
||||
//
|
||||
// required: true
|
||||
Integrity string `json:"integrity"`
|
||||
|
||||
// The script MIME type
|
||||
//
|
||||
// required: true
|
||||
Type string `json:"type"`
|
||||
|
||||
// A unique identifier
|
||||
//
|
||||
// required: true
|
||||
Identifier string `json:"id"`
|
||||
|
||||
// Nonce for CSP
|
||||
//
|
||||
// A nonce you may want to use to improve your Content Security Policy.
|
||||
// You do not have to use this value but if you want to improve your CSP
|
||||
// policies you may use it. You can also choose to use your own nonce value!
|
||||
//
|
||||
// required: true
|
||||
Nonce string `json:"nonce"`
|
||||
|
||||
// NodeType represents this node's types. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "script".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
// DivisionAttributes represent a division section.
|
||||
//
|
||||
// Division sections are used for interactive widgets that require a hook in the DOM / view.
|
||||
//
|
||||
// swagger:model uiNodeDivisionAttributes
|
||||
type DivisionAttributes struct {
|
||||
// A classname that should be rendered into the DOM.
|
||||
Classname string `json:"class,omitzero"`
|
||||
|
||||
// A unique identifier
|
||||
//
|
||||
// required: true
|
||||
Identifier string `json:"id"`
|
||||
|
||||
// Data is a map of key-value pairs that are passed to the division.
|
||||
//
|
||||
// They may be used for `data-...` attributes.
|
||||
Data map[string]string `json:"data,omitzero"`
|
||||
|
||||
// NodeType represents this node's type. It is a mirror of `node.type` and
|
||||
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "script".
|
||||
//
|
||||
// required: true
|
||||
NodeType UiNodeType `json:"node_type"`
|
||||
}
|
||||
|
||||
func (d DivisionAttributes) ID() string {
|
||||
return d.Identifier
|
||||
}
|
||||
|
||||
func (d DivisionAttributes) Reset() {}
|
||||
|
||||
func (d DivisionAttributes) SetValue(_ interface{}) {}
|
||||
|
||||
func (d DivisionAttributes) GetValue() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DivisionAttributes) GetNodeType() UiNodeType {
|
||||
return d.NodeType
|
||||
}
|
||||
|
||||
func (d DivisionAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*DivisionAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && d.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
_ Attributes = new(InputAttributes)
|
||||
_ Attributes = new(ImageAttributes)
|
||||
_ Attributes = new(AnchorAttributes)
|
||||
_ Attributes = new(TextAttributes)
|
||||
_ Attributes = new(ScriptAttributes)
|
||||
_ Attributes = new(DivisionAttributes)
|
||||
)
|
||||
|
||||
func (a *InputAttributes) ID() string {
|
||||
return a.Name
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) ID() string {
|
||||
return a.Identifier
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) ID() string {
|
||||
return a.Identifier
|
||||
}
|
||||
|
||||
func (a *TextAttributes) ID() string {
|
||||
return a.Identifier
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) ID() string {
|
||||
return a.Identifier
|
||||
}
|
||||
|
||||
func (a *InputAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*InputAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && a.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.Type) > 0 && a.Type != ot.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.Name) > 0 && a.Name != ot.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*ImageAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && a.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.Source) > 0 && a.Source != ot.Source {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*AnchorAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && a.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.HREF) > 0 && a.HREF != ot.HREF {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *TextAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*TextAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && a.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) Matches(other Attributes) bool {
|
||||
ot, ok := other.(*ScriptAttributes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ot.ID()) > 0 && a.ID() != ot.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
if ot.Type != "" && a.Type != ot.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
if ot.Source != "" && a.Source != ot.Source {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *InputAttributes) SetValue(value interface{}) {
|
||||
a.FieldValue = value
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) SetValue(value interface{}) {
|
||||
a.Source, _ = value.(string)
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) SetValue(value interface{}) {
|
||||
a.HREF, _ = value.(string)
|
||||
}
|
||||
|
||||
func (a *TextAttributes) SetValue(value interface{}) {
|
||||
a.Text, _ = value.(*text.Message)
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) SetValue(value interface{}) {
|
||||
a.Source, _ = value.(string)
|
||||
}
|
||||
|
||||
func (a *InputAttributes) GetValue() interface{} {
|
||||
return a.FieldValue
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) GetValue() interface{} {
|
||||
return a.Source
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) GetValue() interface{} {
|
||||
return a.HREF
|
||||
}
|
||||
|
||||
func (a *TextAttributes) GetValue() interface{} {
|
||||
return a.Text
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) GetValue() interface{} {
|
||||
return a.Source
|
||||
}
|
||||
|
||||
func (a *InputAttributes) Reset() {
|
||||
a.FieldValue = nil
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) Reset() {
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) Reset() {
|
||||
}
|
||||
|
||||
func (a *TextAttributes) Reset() {
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) Reset() {
|
||||
}
|
||||
|
||||
func (a *InputAttributes) GetNodeType() UiNodeType {
|
||||
return UiNodeType(a.NodeType)
|
||||
}
|
||||
|
||||
func (a *ImageAttributes) GetNodeType() UiNodeType {
|
||||
return UiNodeType(a.NodeType)
|
||||
}
|
||||
|
||||
func (a *AnchorAttributes) GetNodeType() UiNodeType {
|
||||
return UiNodeType(a.NodeType)
|
||||
}
|
||||
|
||||
func (a *TextAttributes) GetNodeType() UiNodeType {
|
||||
return UiNodeType(a.NodeType)
|
||||
}
|
||||
|
||||
func (a *ScriptAttributes) GetNodeType() UiNodeType {
|
||||
return UiNodeType(a.NodeType)
|
||||
}
|
||||
251
ui/node/attributes_input.go
Normal file
251
ui/node/attributes_input.go
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ory/kratos/text"
|
||||
"github.com/ory/kratos/x"
|
||||
"github.com/ory/kratos/x/nosurfx"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
)
|
||||
|
||||
const DisableFormField = "disableFormField"
|
||||
|
||||
// maxEnumOptions caps how many `enum` values a single identity-schema
|
||||
// property may surface as a selectable option on a UI node. The cap bounds
|
||||
// the size of flow responses when an operator (accidentally or otherwise)
|
||||
// declares a very large enum, so a single misconfigured schema cannot
|
||||
// inflate every active registration/settings/login flow on a tenant.
|
||||
const maxEnumOptions = 500
|
||||
|
||||
func toFormType(n string, i interface{}) UiNodeInputAttributeType {
|
||||
switch n {
|
||||
case nosurfx.CSRFTokenName:
|
||||
return InputAttributeTypeHidden
|
||||
case "password":
|
||||
return InputAttributeTypePassword
|
||||
}
|
||||
|
||||
switch i.(type) {
|
||||
case float64, int64, int32, float32, json.Number:
|
||||
return InputAttributeTypeNumber
|
||||
case bool:
|
||||
return InputAttributeTypeCheckbox
|
||||
}
|
||||
|
||||
return InputAttributeTypeText
|
||||
}
|
||||
|
||||
type (
|
||||
InputAttributesModifier func(attributes *InputAttributes)
|
||||
InputAttributesModifiers []InputAttributesModifier
|
||||
)
|
||||
|
||||
func WithRequiredInputAttribute(a *InputAttributes) {
|
||||
a.Required = true
|
||||
}
|
||||
|
||||
func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) {
|
||||
return func(a *InputAttributes) {
|
||||
a.MaxLength = maxLength
|
||||
}
|
||||
}
|
||||
|
||||
func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) {
|
||||
return func(a *InputAttributes) {
|
||||
f(a)
|
||||
}
|
||||
}
|
||||
|
||||
func applyInputAttributes(opts []InputAttributesModifier, attributes *InputAttributes) *InputAttributes {
|
||||
for _, f := range opts {
|
||||
f(attributes)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
type (
|
||||
ImageAttributesModifier func(attributes *ImageAttributes)
|
||||
ImageAttributesModifiers []ImageAttributesModifier
|
||||
)
|
||||
|
||||
func WithImageAttributes(f func(a *ImageAttributes)) func(a *ImageAttributes) {
|
||||
return func(a *ImageAttributes) {
|
||||
f(a)
|
||||
}
|
||||
}
|
||||
|
||||
func applyImageAttributes(opts ImageAttributesModifiers, attributes *ImageAttributes) *ImageAttributes {
|
||||
for _, f := range opts {
|
||||
f(attributes)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
type (
|
||||
ScriptAttributesModifier func(attributes *ScriptAttributes)
|
||||
ScriptAttributesModifiers []ScriptAttributesModifier
|
||||
)
|
||||
|
||||
func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAttributes) *ScriptAttributes {
|
||||
for _, f := range opts {
|
||||
f(attributes)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
type (
|
||||
DivisionAttributesModifier func(attributes *DivisionAttributes)
|
||||
DivisionAttributesModifiers []DivisionAttributesModifier
|
||||
)
|
||||
|
||||
func WithDivisionAttributes(f func(a *DivisionAttributes)) func(a *DivisionAttributes) {
|
||||
return func(a *DivisionAttributes) {
|
||||
f(a)
|
||||
}
|
||||
}
|
||||
|
||||
func applyDivisionAttributes(opts DivisionAttributesModifiers, attributes *DivisionAttributes) *DivisionAttributes {
|
||||
for _, f := range opts {
|
||||
f(attributes)
|
||||
}
|
||||
return attributes
|
||||
}
|
||||
|
||||
func NewInputFieldFromJSON(name string, value interface{}, group UiNodeGroup, opts ...InputAttributesModifier) *Node {
|
||||
return &Node{
|
||||
Type: Input,
|
||||
Group: group,
|
||||
Attributes: applyInputAttributes(opts, &InputAttributes{Name: name, Type: toFormType(name, value), FieldValue: value}),
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewInputField(name string, value interface{}, group UiNodeGroup, inputType UiNodeInputAttributeType, opts ...InputAttributesModifier) *Node {
|
||||
return &Node{
|
||||
Type: Input,
|
||||
Group: group,
|
||||
Attributes: applyInputAttributes(opts, &InputAttributes{Name: name, Type: inputType, FieldValue: value}),
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewImageField(id string, src string, group UiNodeGroup, opts ...ImageAttributesModifier) *Node {
|
||||
return &Node{
|
||||
Type: Image,
|
||||
Group: group,
|
||||
Attributes: applyImageAttributes(opts, &ImageAttributes{Source: src, Identifier: id}),
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewTextField(id string, text *text.Message, group UiNodeGroup) *Node {
|
||||
return &Node{
|
||||
Type: Text,
|
||||
Group: group,
|
||||
Attributes: &TextAttributes{Text: text, Identifier: id},
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewDivisionField(id string, group UiNodeGroup, opts ...DivisionAttributesModifier) *Node {
|
||||
return &Node{
|
||||
Type: Division,
|
||||
Group: group,
|
||||
Attributes: applyDivisionAttributes(opts, &DivisionAttributes{Identifier: id}),
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewAnchorField(id string, href string, group UiNodeGroup, title *text.Message) *Node {
|
||||
return &Node{
|
||||
Type: Anchor,
|
||||
Group: group,
|
||||
Attributes: &AnchorAttributes{Title: title, HREF: href, Identifier: id},
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewScriptField(name string, src string, group UiNodeGroup, integrity string, opts ...ScriptAttributesModifier) *Node {
|
||||
return &Node{
|
||||
Type: Script,
|
||||
Group: group,
|
||||
Attributes: applyScriptAttributes(opts, &ScriptAttributes{
|
||||
Identifier: name,
|
||||
Type: "text/javascript",
|
||||
Source: src,
|
||||
Async: true,
|
||||
ReferrerPolicy: "no-referrer",
|
||||
CrossOrigin: "anonymous",
|
||||
Integrity: integrity,
|
||||
Nonce: x.NewUUID().String(),
|
||||
}),
|
||||
Meta: &Meta{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewInputFieldFromSchema(name string, group UiNodeGroup, p jsonschemax.Path, opts ...InputAttributesModifier) *Node {
|
||||
attr := &InputAttributes{
|
||||
Name: name,
|
||||
Type: toFormType(p.Name, p.Type),
|
||||
Required: p.Required,
|
||||
}
|
||||
|
||||
// If format is set, we can make a more distinct decision:
|
||||
switch p.Format {
|
||||
case "date-time":
|
||||
attr.Type = InputAttributeTypeDateTimeLocal
|
||||
case "email":
|
||||
attr.Type = InputAttributeTypeEmail
|
||||
attr.Autocomplete = InputAttributeAutocompleteEmail
|
||||
case "tel":
|
||||
attr.Type = InputAttributeTypeTel
|
||||
attr.Autocomplete = InputAttributeAutocompleteTel
|
||||
case "date":
|
||||
attr.Type = InputAttributeTypeDate
|
||||
case "uri":
|
||||
attr.Type = InputAttributeTypeURI
|
||||
attr.Autocomplete = InputAttributeAutocompleteUrl
|
||||
case "regex":
|
||||
attr.Type = InputAttributeTypeText
|
||||
}
|
||||
|
||||
// Enum values become selectable options. The presence of options is the
|
||||
// signal for consumers to render a dropdown instead of a free-form input.
|
||||
// Cap the number of options to keep a pathological schema from inflating
|
||||
// every flow response.
|
||||
if len(p.Enum) > 0 {
|
||||
n := min(len(p.Enum), maxEnumOptions)
|
||||
attr.Options = make([]InputAttributesOption, n)
|
||||
for i, v := range p.Enum[:n] {
|
||||
attr.Options[i] = InputAttributesOption{Value: v}
|
||||
}
|
||||
}
|
||||
|
||||
// Other properties
|
||||
if p.Pattern != nil {
|
||||
attr.Pattern = p.Pattern.String()
|
||||
}
|
||||
|
||||
// Set disabled if the custom property is set
|
||||
if isDisabled, ok := p.CustomProperties[DisableFormField]; ok {
|
||||
if isDisabled, ok := isDisabled.(bool); ok {
|
||||
attr.Disabled = isDisabled
|
||||
}
|
||||
}
|
||||
|
||||
var meta Meta
|
||||
if len(p.Title) > 0 {
|
||||
meta.Label = text.NewInfoNodeLabelGenerated(p.Title, name)
|
||||
}
|
||||
|
||||
return &Node{
|
||||
Type: Input,
|
||||
Attributes: applyInputAttributes(opts, attr),
|
||||
Group: group,
|
||||
Meta: &meta,
|
||||
}
|
||||
}
|
||||
19
ui/node/attributes_input_csrf.go
Normal file
19
ui/node/attributes_input_csrf.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import "github.com/ory/kratos/x/nosurfx"
|
||||
|
||||
func NewCSRFNode(token string) *Node {
|
||||
return &Node{
|
||||
Type: Input,
|
||||
Group: DefaultGroup,
|
||||
Attributes: &InputAttributes{
|
||||
Name: nosurfx.CSRFTokenName,
|
||||
Type: InputAttributeTypeHidden,
|
||||
FieldValue: token,
|
||||
Required: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
100
ui/node/attributes_input_test.go
Normal file
100
ui/node/attributes_input_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
)
|
||||
|
||||
func TestFieldFromPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
t.Run("all properties are properly transferred", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
schema, err := os.ReadFile("./fixtures/all_formats.schema.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
c := jsonschema.NewCompiler()
|
||||
require.NoError(t, c.AddResource("test.json", bytes.NewBuffer(schema)))
|
||||
|
||||
paths, err := jsonschemax.ListPaths(ctx, "test.json", c)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, path := range paths {
|
||||
node := NewInputFieldFromSchema(path.Name, DefaultGroup, path)
|
||||
assert.EqualValues(t, "input", node.Type)
|
||||
require.IsType(t, new(InputAttributes), node.Attributes)
|
||||
attr := node.Attributes.(*InputAttributes)
|
||||
|
||||
assert.EqualValues(t, gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_type", path.Name)).String(), attr.Type)
|
||||
assert.True(t, !gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_pattern", path.Name)).Exists() ||
|
||||
(gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_pattern", path.Name)).Bool() && attr.Pattern != ""))
|
||||
|
||||
expectedAutocomplete := gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_autocomplete", path.Name))
|
||||
|
||||
if expectedAutocomplete.Exists() {
|
||||
assert.EqualValues(t, expectedAutocomplete.String(), attr.Autocomplete)
|
||||
}
|
||||
|
||||
expectedOptions := gjson.GetBytes(schema, fmt.Sprintf("properties.%s.test_expected_options", path.Name))
|
||||
if expectedOptions.Exists() {
|
||||
// Compare through JSON so string, number, and boolean
|
||||
// option values all survive the round-trip without relying
|
||||
// on a lossy `%v` coercion.
|
||||
var got []any
|
||||
for _, o := range attr.Options {
|
||||
got = append(got, o.Value)
|
||||
}
|
||||
gotJSON, err := json.Marshal(got)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, expectedOptions.Raw, string(gotJSON), "field %s", path.Name)
|
||||
} else {
|
||||
assert.Empty(t, attr.Options, "field %s should have no options", path.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("caps enum options at maxEnumOptions", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
enum := make([]any, maxEnumOptions+50)
|
||||
for i := range enum {
|
||||
enum[i] = fmt.Sprintf("v%d", i)
|
||||
}
|
||||
node := NewInputFieldFromSchema("field", DefaultGroup, jsonschemax.Path{
|
||||
Name: "field",
|
||||
Type: "",
|
||||
Enum: enum,
|
||||
})
|
||||
attr := node.Attributes.(*InputAttributes)
|
||||
assert.Len(t, attr.Options, maxEnumOptions)
|
||||
assert.Equal(t, "v0", attr.Options[0].Value)
|
||||
assert.Equal(t, fmt.Sprintf("v%d", maxEnumOptions-1), attr.Options[maxEnumOptions-1].Value)
|
||||
})
|
||||
|
||||
t.Run("empty enum produces no options", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := NewInputFieldFromSchema("field", DefaultGroup, jsonschemax.Path{
|
||||
Name: "field",
|
||||
Type: "",
|
||||
Enum: []any{},
|
||||
})
|
||||
attr := node.Attributes.(*InputAttributes)
|
||||
assert.Empty(t, attr.Options)
|
||||
})
|
||||
}
|
||||
124
ui/node/attributes_test.go
Normal file
124
ui/node/attributes_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/x/jsonx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIDs(t *testing.T) {
|
||||
assert.EqualValues(t, "foo", (&AnchorAttributes{Identifier: "foo"}).ID())
|
||||
assert.EqualValues(t, "foo", (&ImageAttributes{Identifier: "foo"}).ID())
|
||||
assert.EqualValues(t, "foo", (&TextAttributes{Identifier: "foo"}).ID())
|
||||
assert.EqualValues(t, "foo", (&InputAttributes{Name: "foo"}).ID())
|
||||
assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID())
|
||||
}
|
||||
|
||||
func TestMatchesAnchorAttributes(t *testing.T) {
|
||||
assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"}))
|
||||
assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"}))
|
||||
assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
|
||||
assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
|
||||
|
||||
assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"}))
|
||||
assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"}))
|
||||
assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"}))
|
||||
|
||||
assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
|
||||
}
|
||||
|
||||
func TestMatchesImageAttributes(t *testing.T) {
|
||||
assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
|
||||
assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"}))
|
||||
assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
|
||||
assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
|
||||
|
||||
assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"}))
|
||||
assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"}))
|
||||
assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"}))
|
||||
|
||||
assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
|
||||
}
|
||||
|
||||
func TestMatchesInputAttributes(t *testing.T) {
|
||||
// Test when other is not of type *InputAttributes
|
||||
var attr Attributes = &ImageAttributes{}
|
||||
inputAttr := &InputAttributes{Name: "foo"}
|
||||
assert.False(t, inputAttr.Matches(attr))
|
||||
|
||||
// Test when ID is different
|
||||
attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
|
||||
inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
|
||||
assert.False(t, inputAttr.Matches(attr))
|
||||
|
||||
// Test when Type is different
|
||||
attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
|
||||
inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber}
|
||||
assert.False(t, inputAttr.Matches(attr))
|
||||
|
||||
// Test when FieldValue is different
|
||||
attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
|
||||
inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"}
|
||||
assert.False(t, inputAttr.Matches(attr))
|
||||
|
||||
// Test when Name is different
|
||||
attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
|
||||
inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
|
||||
assert.False(t, inputAttr.Matches(attr))
|
||||
|
||||
// Test when all fields are the same
|
||||
attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
|
||||
inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
|
||||
assert.True(t, inputAttr.Matches(attr))
|
||||
}
|
||||
|
||||
func TestMatchesTextAttributes(t *testing.T) {
|
||||
assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
|
||||
assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
|
||||
assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
|
||||
}
|
||||
|
||||
func TestNodeEncode(t *testing.T) {
|
||||
script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}})
|
||||
assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String())
|
||||
assert.EqualValues(t, Script, gjson.Get(script, "type").String())
|
||||
|
||||
text := jsonx.TestMarshalJSONString(t, &Node{Attributes: &TextAttributes{}})
|
||||
assert.EqualValues(t, Text, gjson.Get(text, "attributes.node_type").String())
|
||||
assert.EqualValues(t, Text, gjson.Get(text, "type").String())
|
||||
|
||||
image := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ImageAttributes{}})
|
||||
assert.EqualValues(t, Image, gjson.Get(image, "attributes.node_type").String())
|
||||
assert.EqualValues(t, Image, gjson.Get(image, "type").String())
|
||||
|
||||
input := jsonx.TestMarshalJSONString(t, &Node{Attributes: &InputAttributes{}})
|
||||
assert.EqualValues(t, Input, gjson.Get(input, "attributes.node_type").String())
|
||||
assert.EqualValues(t, Input, gjson.Get(input, "type").String())
|
||||
|
||||
anchor := jsonx.TestMarshalJSONString(t, &Node{Attributes: &AnchorAttributes{}})
|
||||
assert.EqualValues(t, Anchor, gjson.Get(anchor, "attributes.node_type").String())
|
||||
assert.EqualValues(t, Anchor, gjson.Get(anchor, "type").String())
|
||||
}
|
||||
|
||||
func TestNodeDecode(t *testing.T) {
|
||||
for _, kind := range []UiNodeType{
|
||||
Text,
|
||||
Input,
|
||||
Image,
|
||||
Anchor,
|
||||
Script,
|
||||
} {
|
||||
var n Node
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"type":"`+kind+`"}`), &n))
|
||||
assert.EqualValues(t, kind, n.Attributes.GetNodeType())
|
||||
}
|
||||
}
|
||||
92
ui/node/fixtures/all_formats.schema.json
Normal file
92
ui/node/fixtures/all_formats.schema.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"$id": "https://example.com/all_formats.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string",
|
||||
"test_expected_type": "password"
|
||||
},
|
||||
"csrf_token": {
|
||||
"type": "string",
|
||||
"test_expected_type": "hidden"
|
||||
},
|
||||
"simpleString": {
|
||||
"type": "string",
|
||||
"test_expected_type": "text"
|
||||
},
|
||||
"simpleNumber": {
|
||||
"type": "number",
|
||||
"test_expected_type": "number"
|
||||
},
|
||||
"simpleBoolean": {
|
||||
"type": "boolean",
|
||||
"test_expected_type": "checkbox"
|
||||
},
|
||||
"emailString": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"test_expected_type": "email",
|
||||
"test_expected_autocomplete": "email"
|
||||
},
|
||||
"phoneString": {
|
||||
"type": "string",
|
||||
"format": "tel",
|
||||
"test_expected_type": "tel",
|
||||
"test_expected_autocomplete": "tel"
|
||||
},
|
||||
"dateTimeString": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"test_expected_type": "datetime-local"
|
||||
},
|
||||
"regexString": {
|
||||
"type": "string",
|
||||
"format": "regex",
|
||||
"test_expected_type": "text"
|
||||
},
|
||||
"dateString": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"test_expected_type": "date"
|
||||
},
|
||||
"uriString": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"test_expected_type": "url",
|
||||
"test_expected_autocomplete": "url"
|
||||
},
|
||||
"patternString": {
|
||||
"type": "string",
|
||||
"pattern": "foo.*",
|
||||
"test_expected_type": "text",
|
||||
"test_expected_pattern": true
|
||||
},
|
||||
"enumString": {
|
||||
"type": "string",
|
||||
"enum": ["one", "two", "three"],
|
||||
"test_expected_type": "text",
|
||||
"test_expected_options": ["one", "two", "three"]
|
||||
},
|
||||
"enumNumber": {
|
||||
"type": "number",
|
||||
"enum": [1, 2.5, 3],
|
||||
"test_expected_type": "number",
|
||||
"test_expected_options": [1, 2.5, 3]
|
||||
},
|
||||
"enumBoolean": {
|
||||
"type": "boolean",
|
||||
"enum": [true, false],
|
||||
"test_expected_type": "checkbox",
|
||||
"test_expected_options": [true, false]
|
||||
},
|
||||
"enumEmail": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"enum": ["admin@example.com", "user@example.com"],
|
||||
"test_expected_type": "email",
|
||||
"test_expected_autocomplete": "email",
|
||||
"test_expected_options": ["admin@example.com", "user@example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
52
ui/node/fixtures/complex.schema.json
Normal file
52
ui/node/fixtures/complex.schema.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$id": "https://example.com/complex.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["meal"],
|
||||
"properties": {
|
||||
"meal": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"chef": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fruits": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"vegetables": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/veggie"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"veggie": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"veggieName",
|
||||
"veggieLike"
|
||||
],
|
||||
"properties": {
|
||||
"veggieName": {
|
||||
"type": "string"
|
||||
},
|
||||
"veggieLike": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"veggieAmount": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ui/node/fixtures/identity.schema.json
Normal file
43
ui/node/fixtures/identity.schema.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$id": "https://example.com/registration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"via": "email"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stringy": {
|
||||
"type": "string"
|
||||
},
|
||||
"numby": {
|
||||
"type": "number"
|
||||
},
|
||||
"booly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"should_big_number": {
|
||||
"type": "number",
|
||||
"minimum": 1200
|
||||
},
|
||||
"should_long_string": {
|
||||
"type": "string",
|
||||
"minLength": 25
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
ui/node/fixtures/simple.schema.json
Normal file
29
ui/node/fixtures/simple.schema.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$id": "https://example.com/registration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stringy": {
|
||||
"type": "string"
|
||||
},
|
||||
"numby": {
|
||||
"type": "number"
|
||||
},
|
||||
"objy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stringy": {
|
||||
"type": "string"
|
||||
},
|
||||
"numby": {
|
||||
"type": "number"
|
||||
},
|
||||
"objy": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
ui/node/fixtures/sort/expected/1.json
Normal file
108
ui/node/fixtures/sort/expected/1.json
Normal file
@@ -0,0 +1,108 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "Sk3WySQDVpL5V9IuQelZTXcRVbBE1STI0HAJEcHOnM0/xPz5p1piBzuBMKZWxgYJc5TN8k8BTM9Hazrf/n76pg==",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password_identifier",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
118
ui/node/fixtures/sort/expected/2.json
Normal file
118
ui/node/fixtures/sort/expected/2.json
Normal file
@@ -0,0 +1,118 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.abcd",
|
||||
"type": "input",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
105
ui/node/fixtures/sort/expected/3.json
Normal file
105
ui/node/fixtures/sort/expected/3.json
Normal file
@@ -0,0 +1,105 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.abcd",
|
||||
"type": "input",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
164
ui/node/fixtures/sort/expected/4.json
Normal file
164
ui/node/fixtures/sort/expected/4.json
Normal file
@@ -0,0 +1,164 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "profile",
|
||||
"attributes": {
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"value": "john@doe.com",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "profile",
|
||||
"attributes": {
|
||||
"name": "traits.name",
|
||||
"type": "text",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "profile",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "profile",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070003,
|
||||
"text": "Save",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070001,
|
||||
"text": "Password",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070003,
|
||||
"text": "Save",
|
||||
"type": "info"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "link",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1050002,
|
||||
"text": "Link github",
|
||||
"type": "info",
|
||||
"context": {
|
||||
"provider": "github"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "unlink",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1050003,
|
||||
"text": "Unlink google",
|
||||
"type": "info",
|
||||
"context": {
|
||||
"provider": "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "link",
|
||||
"type": "submit",
|
||||
"value": "ory",
|
||||
"disabled": false,
|
||||
"node_type": "input"
|
||||
},
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1050002,
|
||||
"text": "Link ory",
|
||||
"type": "info",
|
||||
"context": {
|
||||
"provider": "ory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
92
ui/node/fixtures/sort/input/1.json
Normal file
92
ui/node/fixtures/sort/input/1.json
Normal file
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password_identifier",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "Sk3WySQDVpL5V9IuQelZTXcRVbBE1STI0HAJEcHOnM0/xPz5p1piBzuBMKZWxgYJc5TN8k8BTM9Hazrf/n76pg==",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
}
|
||||
]
|
||||
100
ui/node/fixtures/sort/input/2.json
Normal file
100
ui/node/fixtures/sort/input/2.json
Normal file
@@ -0,0 +1,100 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.abcd",
|
||||
"type": "input",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
}
|
||||
]
|
||||
89
ui/node/fixtures/sort/input/3.json
Normal file
89
ui/node/fixtures/sort/input/3.json
Normal file
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "oidc",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "default",
|
||||
"attributes": {
|
||||
"name": "csrf_token",
|
||||
"type": "hidden",
|
||||
"value": "qomtJu6UOzh5Gqkq3MEwbbJuNgJEfDg9gEowfMH0OhjfAIcWbc0PrbvMS6LL7m8ptuuuQE+oUDoXUQOy/kRccw==",
|
||||
"required": true,
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "github",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "google",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "oidc",
|
||||
"attributes": {
|
||||
"name": "oidc.provider",
|
||||
"type": "submit",
|
||||
"value": "hydra",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"group": "password",
|
||||
"attributes": {
|
||||
"name": "traits.abcd",
|
||||
"type": "input",
|
||||
"disabled": false
|
||||
},
|
||||
"messages": null
|
||||
}
|
||||
]
|
||||
155
ui/node/fixtures/sort/input/4.json
Normal file
155
ui/node/fixtures/sort/input/4.json
Normal file
@@ -0,0 +1,155 @@
|
||||
[
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "csrf_token",
|
||||
"required": true,
|
||||
"type": "hidden",
|
||||
"value": ""
|
||||
},
|
||||
"group": "default",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "traits.email",
|
||||
"type": "email",
|
||||
"value": "john@doe.com"
|
||||
},
|
||||
"group": "profile",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "traits.name",
|
||||
"type": "text"
|
||||
},
|
||||
"group": "profile",
|
||||
"messages": [],
|
||||
"meta": {},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "profile"
|
||||
},
|
||||
"group": "profile",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070003,
|
||||
"text": "Save",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "password",
|
||||
"required": true,
|
||||
"type": "password"
|
||||
},
|
||||
"group": "password",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070001,
|
||||
"text": "Password",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "method",
|
||||
"type": "submit",
|
||||
"value": "password"
|
||||
},
|
||||
"group": "password",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"id": 1070003,
|
||||
"text": "Save",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "link",
|
||||
"type": "submit",
|
||||
"value": "github"
|
||||
},
|
||||
"group": "oidc",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"context": {
|
||||
"provider": "github"
|
||||
},
|
||||
"id": 1050002,
|
||||
"text": "Link github",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "unlink",
|
||||
"type": "submit",
|
||||
"value": "google"
|
||||
},
|
||||
"group": "oidc",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"context": {
|
||||
"provider": "google"
|
||||
},
|
||||
"id": 1050003,
|
||||
"text": "Unlink google",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
},
|
||||
{
|
||||
"attributes": {
|
||||
"disabled": false,
|
||||
"name": "link",
|
||||
"type": "submit",
|
||||
"value": "ory"
|
||||
},
|
||||
"group": "oidc",
|
||||
"messages": [],
|
||||
"meta": {
|
||||
"label": {
|
||||
"context": {
|
||||
"provider": "ory"
|
||||
},
|
||||
"id": 1050002,
|
||||
"text": "Link ory",
|
||||
"type": "info"
|
||||
}
|
||||
},
|
||||
"type": "input"
|
||||
}
|
||||
]
|
||||
39
ui/node/fixtures/sort/schema/2.json
Normal file
39
ui/node/fixtures/sort/schema/2.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"abcd": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "E-Mail",
|
||||
"minLength": 3,
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"minLength": 10
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email",
|
||||
"website"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
39
ui/node/fixtures/sort/schema/3.json
Normal file
39
ui/node/fixtures/sort/schema/3.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"abcd": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"title": "E-Mail",
|
||||
"minLength": 3,
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"minLength": 10
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email",
|
||||
"website"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
32
ui/node/fixtures/sort/schema/4.json
Normal file
32
ui/node/fixtures/sort/schema/4.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$id": "https://example.com/person.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Person",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"format": "email",
|
||||
"type": "string",
|
||||
"ory.sh/kratos": {
|
||||
"credentials": {
|
||||
"password": {
|
||||
"identifier": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 2
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
14
ui/node/helper.go
Normal file
14
ui/node/helper.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
func PasswordLoginOrder(in []string) []string {
|
||||
if len(in) == 0 {
|
||||
return []string{"csrf_token", "password"}
|
||||
}
|
||||
if len(in) == 1 {
|
||||
return append([]string{"csrf_token"}, in[0], "password")
|
||||
}
|
||||
return append([]string{"csrf_token", in[0], "password"}, in[1:]...)
|
||||
}
|
||||
50
ui/node/identifiers.go
Normal file
50
ui/node/identifiers.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
const (
|
||||
TOTPCode = "totp_code"
|
||||
TOTPSecretKey = "totp_secret_key"
|
||||
TOTPQR = "totp_qr"
|
||||
TOTPUnlink = "totp_unlink"
|
||||
)
|
||||
|
||||
const (
|
||||
LookupReveal = "lookup_secret_reveal"
|
||||
LookupRegenerate = "lookup_secret_regenerate"
|
||||
LookupDisable = "lookup_secret_disable"
|
||||
LookupCodes = "lookup_secret_codes"
|
||||
LookupConfirm = "lookup_secret_confirm"
|
||||
LookupCodeEnter = "lookup_secret"
|
||||
)
|
||||
|
||||
const (
|
||||
ProfileChooseCredentials = "profile_choose_credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
WebAuthnRegisterTrigger = "webauthn_register_trigger"
|
||||
WebAuthnRegister = "webauthn_register"
|
||||
WebAuthnLogin = "webauthn_login"
|
||||
WebAuthnLoginTrigger = "webauthn_login_trigger"
|
||||
WebAuthnRegisterDisplayName = "webauthn_register_displayname"
|
||||
WebAuthnRemove = "webauthn_remove"
|
||||
WebAuthnScript = "webauthn_script"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceAuthnRemove = "deviceauthn_remove"
|
||||
DeviceAuthnNonce = "deviceauthn_nonce"
|
||||
)
|
||||
|
||||
const (
|
||||
PasskeyRegisterTrigger = "passkey_register_trigger"
|
||||
PasskeyRegister = "passkey_register"
|
||||
PasskeySettingsRegister = "passkey_settings_register"
|
||||
PasskeyCreateData = "passkey_create_data"
|
||||
PasskeyLogin = "passkey_login"
|
||||
PasskeyChallenge = "passkey_challenge"
|
||||
PasskeyLoginTrigger = "passkey_login_trigger" //#nosec G101 -- Not a credential
|
||||
PasskeyRemove = "passkey_remove"
|
||||
)
|
||||
577
ui/node/node.go
Normal file
577
ui/node/node.go
Normal file
@@ -0,0 +1,577 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/kratos/schema"
|
||||
|
||||
"github.com/ory/kratos/text"
|
||||
)
|
||||
|
||||
// swagger:enum UiNodeType
|
||||
type UiNodeType string
|
||||
|
||||
const (
|
||||
Text UiNodeType = "text"
|
||||
Input UiNodeType = "input"
|
||||
Image UiNodeType = "img"
|
||||
Anchor UiNodeType = "a"
|
||||
Script UiNodeType = "script"
|
||||
Division UiNodeType = "div"
|
||||
)
|
||||
|
||||
func (t UiNodeType) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
// swagger:enum UiNodeGroup
|
||||
type UiNodeGroup string
|
||||
|
||||
const (
|
||||
DefaultGroup UiNodeGroup = "default"
|
||||
PasswordGroup UiNodeGroup = "password"
|
||||
OpenIDConnectGroup UiNodeGroup = "oidc"
|
||||
ProfileGroup UiNodeGroup = "profile"
|
||||
LinkGroup UiNodeGroup = "link"
|
||||
CodeGroup UiNodeGroup = "code"
|
||||
TOTPGroup UiNodeGroup = "totp"
|
||||
LookupGroup UiNodeGroup = "lookup_secret"
|
||||
WebAuthnGroup UiNodeGroup = "webauthn"
|
||||
PasskeyGroup UiNodeGroup = "passkey"
|
||||
IdentifierFirstGroup UiNodeGroup = "identifier_first"
|
||||
CaptchaGroup UiNodeGroup = "captcha" // Available in OEL
|
||||
SAMLGroup UiNodeGroup = "saml" // Available in OEL
|
||||
DeviceAuthnGroup UiNodeGroup = "deviceauthn" // Available in OEL
|
||||
)
|
||||
|
||||
func (g UiNodeGroup) String() string {
|
||||
return string(g)
|
||||
}
|
||||
|
||||
// swagger:model uiNodes
|
||||
type Nodes []*Node
|
||||
|
||||
// Node represents a flow's nodes
|
||||
//
|
||||
// Nodes are represented as HTML elements or their native UI equivalents. For example,
|
||||
// a node can be an `<img>` tag, or an `<input element>` but also `some plain text`.
|
||||
//
|
||||
// swagger:model uiNode
|
||||
type Node struct {
|
||||
// The node's type
|
||||
//
|
||||
// required: true
|
||||
Type UiNodeType `json:"type" faker:"-"`
|
||||
|
||||
// Group specifies which group (e.g. password authenticator) this node belongs to.
|
||||
//
|
||||
// required: true
|
||||
Group UiNodeGroup `json:"group"`
|
||||
|
||||
// The node's attributes.
|
||||
//
|
||||
// required: true
|
||||
// swagger:type uiNodeAttributes
|
||||
Attributes Attributes `json:"attributes" faker:"ui_node_attributes"`
|
||||
|
||||
// The node's messages
|
||||
//
|
||||
// Contains error, validation, or other messages relevant to this node.
|
||||
//
|
||||
// required: true
|
||||
Messages text.Messages `json:"messages"`
|
||||
|
||||
// Meta contains a node meta information
|
||||
//
|
||||
// This might include a label and other information that can optionally
|
||||
// be used to render UIs.
|
||||
//
|
||||
// required: true
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// A Node's Meta Information
|
||||
//
|
||||
// This might include a label and other information that can optionally
|
||||
// be used to render UIs.
|
||||
//
|
||||
// swagger:model uiNodeMeta
|
||||
type Meta struct {
|
||||
// Label represents the node's label.
|
||||
//
|
||||
// Keep in mind that these values are autogenerated and can not be changed.
|
||||
// If you wish to use other titles or labels implement that directly in
|
||||
// your UI.
|
||||
Label *text.Message `json:"label,omitempty"`
|
||||
}
|
||||
|
||||
// Used for en/decoding the Attributes field.
|
||||
type jsonRawNode struct {
|
||||
Type UiNodeType `json:"type"`
|
||||
Group UiNodeGroup `json:"group"`
|
||||
Attributes Attributes `json:"attributes"`
|
||||
Messages text.Messages `json:"messages"`
|
||||
Meta *Meta `json:"meta"`
|
||||
}
|
||||
|
||||
func (n *Node) ID() string {
|
||||
return n.Attributes.ID()
|
||||
}
|
||||
|
||||
func (n *Node) Reset() {
|
||||
n.Messages = nil
|
||||
n.Attributes.Reset()
|
||||
}
|
||||
|
||||
func (n *Node) WithMetaLabel(label *text.Message) *Node {
|
||||
if n.Meta == nil {
|
||||
n.Meta = new(Meta)
|
||||
}
|
||||
n.Meta.Label = label
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *Node) GetValue() interface{} {
|
||||
return n.Attributes.GetValue()
|
||||
}
|
||||
|
||||
func (n Nodes) Find(id string) *Node {
|
||||
for _, nn := range n {
|
||||
if nn.ID() == id {
|
||||
return nn
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n Nodes) FindAll(id string) []Node {
|
||||
var nodes []Node
|
||||
for _, nn := range n {
|
||||
if nn.ID() == id {
|
||||
nodes = append(nodes, *nn)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (n Nodes) Reset(exclude ...string) {
|
||||
for k, nn := range n {
|
||||
nn.Messages = nil
|
||||
if !slices.Contains(exclude, nn.ID()) {
|
||||
nn.Reset()
|
||||
}
|
||||
n[k] = nn
|
||||
}
|
||||
}
|
||||
|
||||
func (n Nodes) ResetNodes(reset ...string) {
|
||||
for k, nn := range n {
|
||||
if slices.Contains(reset, nn.ID()) {
|
||||
nn.Reset()
|
||||
}
|
||||
n[k] = nn
|
||||
}
|
||||
}
|
||||
|
||||
func (n Nodes) ResetNodesWithPrefix(prefix string) {
|
||||
for k := range n {
|
||||
if strings.HasPrefix(n[k].ID(), prefix) {
|
||||
n[k].Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getStringSliceIndexOf(needle []string, haystack string) int {
|
||||
for k := range needle {
|
||||
if needle[k] == haystack {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type sortOptions struct {
|
||||
orderByGroups []string
|
||||
schemaRef string
|
||||
keysInOrder []string
|
||||
keysInOrderAppend []string
|
||||
keysInOrderPost func([]string) []string
|
||||
stableGroups []string
|
||||
}
|
||||
|
||||
type SortOption func(*sortOptions)
|
||||
|
||||
func SortByGroups(orderByGroups []UiNodeGroup) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.orderByGroups = make([]string, len(orderByGroups))
|
||||
for k := range orderByGroups {
|
||||
options.orderByGroups[k] = string(orderByGroups[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SortBySchema(schemaRef string) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.schemaRef = schemaRef
|
||||
}
|
||||
}
|
||||
|
||||
func SortUseOrder(keysInOrder []string) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.keysInOrder = keysInOrder
|
||||
}
|
||||
}
|
||||
|
||||
func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.keysInOrderAppend = keysInOrder
|
||||
}
|
||||
}
|
||||
|
||||
func SortUpdateOrder(f func([]string) []string) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.keysInOrderPost = f
|
||||
}
|
||||
}
|
||||
|
||||
// SortStableGroups marks groups that arrive in a deterministic order and must
|
||||
// keep it: within such a group the sort preserves insertion order instead of
|
||||
// tiebreaking by value. Use it when the repeated nodes' only distinguishing
|
||||
// value is opaque and non-display, so value ordering would be meaningless and unstable.
|
||||
func SortStableGroups(groups ...UiNodeGroup) func(*sortOptions) {
|
||||
return func(options *sortOptions) {
|
||||
options.stableGroups = make([]string, len(groups))
|
||||
for k := range groups {
|
||||
options.stableGroups[k] = string(groups[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n Nodes) SortBySchema(ctx context.Context, opts ...SortOption) error {
|
||||
var o sortOptions
|
||||
for _, f := range opts {
|
||||
f(&o)
|
||||
}
|
||||
|
||||
if o.schemaRef != "" {
|
||||
schemaKeys, err := schema.GetKeysInOrder(ctx, o.schemaRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.keysInOrder = append(o.keysInOrder, schemaKeys...)
|
||||
}
|
||||
|
||||
if o.keysInOrderPost != nil {
|
||||
o.keysInOrder = o.keysInOrderPost(o.keysInOrder)
|
||||
}
|
||||
|
||||
o.keysInOrder = append(o.keysInOrder, o.keysInOrderAppend...)
|
||||
|
||||
getKeyPosition := func(node *Node) int {
|
||||
lastPrefix := len(o.keysInOrder)
|
||||
|
||||
// Method should always be the last element in the list
|
||||
if node.Attributes.ID() == "method" {
|
||||
return len(n) + len(o.keysInOrder) + 1
|
||||
}
|
||||
|
||||
for i, n := range o.keysInOrder {
|
||||
if strings.HasPrefix(node.ID(), n) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return lastPrefix
|
||||
}
|
||||
|
||||
if len(o.orderByGroups) > 0 {
|
||||
// Sort by groups so that default is in front, then oidc, password, ...
|
||||
// A stable sort keeps same-group nodes in their incoming order, so a
|
||||
// group listed in stableGroups is not reshuffled before the tiebreak
|
||||
// below has a chance to preserve it.
|
||||
sort.SliceStable(n, func(i, j int) bool {
|
||||
a := string(n[i].Group)
|
||||
b := string(n[j].Group)
|
||||
return getStringSliceIndexOf(o.orderByGroups, a) < getStringSliceIndexOf(o.orderByGroups, b)
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(n, func(i, j int) bool {
|
||||
a := n[i]
|
||||
b := n[j]
|
||||
|
||||
if a.Group == b.Group ||
|
||||
(a.Group == "default" && b.Group == "password") || (b.Group == "default" && a.Group == "password") {
|
||||
pa, pb := getKeyPosition(a), getKeyPosition(b)
|
||||
if pa < pb {
|
||||
return true
|
||||
} else if pa > pb {
|
||||
return false
|
||||
}
|
||||
|
||||
// A stable group provides its own deterministic order; keep it
|
||||
// rather than tiebreaking by the (meaningless, possibly unstable)
|
||||
// value.
|
||||
if a.Group == b.Group && slices.Contains(o.stableGroups, string(a.Group)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v", a.GetValue()) < fmt.Sprintf("%v", b.GetValue())
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes one or more nodes by their IDs.
|
||||
func (n *Nodes) Remove(ids ...string) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var r Nodes
|
||||
for k, v := range *n {
|
||||
var found bool
|
||||
for _, needle := range ids {
|
||||
if (*n)[k].ID() == needle {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
r = append(r, v)
|
||||
}
|
||||
}
|
||||
*n = r
|
||||
}
|
||||
|
||||
// RemoveInGroup removes nodes whose ID is one of ids and that belong to the
|
||||
// given group. Unlike Remove, which matches on ID alone, this leaves nodes with
|
||||
// the same ID in other groups untouched — so one strategy clearing its own
|
||||
// link/unlink nodes does not clobber another strategy's nodes that share the
|
||||
// "link"/"unlink" input name but render in a different group.
|
||||
func (n *Nodes) RemoveInGroup(group UiNodeGroup, ids ...string) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var r Nodes
|
||||
for _, v := range *n {
|
||||
if v != nil && v.Group == group && slices.Contains(ids, v.ID()) {
|
||||
continue
|
||||
}
|
||||
r = append(r, v)
|
||||
}
|
||||
*n = r
|
||||
}
|
||||
|
||||
// RemoveGroup removes all nodes belonging to a specific group.
|
||||
func (n *Nodes) RemoveGroup(group UiNodeGroup) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var r Nodes
|
||||
for k, v := range *n {
|
||||
if (*n)[k] != nil && string((*n)[k].Group) != string(group) {
|
||||
r = append(r, v)
|
||||
}
|
||||
}
|
||||
*n = r
|
||||
}
|
||||
|
||||
// RemoveGroup removes all nodes belonging to a set of groups.
|
||||
func (n *Nodes) RemoveGroups(group ...UiNodeGroup) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, g := range group {
|
||||
n.RemoveGroup(g)
|
||||
}
|
||||
}
|
||||
|
||||
// ClearTransientNodes removes all nodes that belong to transient groups.
|
||||
func (n *Nodes) ClearTransientNodes() {
|
||||
n.RemoveGroups(
|
||||
DefaultGroup,
|
||||
PasswordGroup,
|
||||
OpenIDConnectGroup,
|
||||
ProfileGroup,
|
||||
LinkGroup,
|
||||
CodeGroup,
|
||||
TOTPGroup,
|
||||
LookupGroup,
|
||||
WebAuthnGroup,
|
||||
PasskeyGroup,
|
||||
IdentifierFirstGroup,
|
||||
SAMLGroup,
|
||||
)
|
||||
}
|
||||
|
||||
// Upsert updates or appends a node.
|
||||
func (n *Nodes) Upsert(node *Node) {
|
||||
if n == nil {
|
||||
*n = append(*n, node)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range *n {
|
||||
if (*n)[i].ID() == node.ID() {
|
||||
(*n)[i] = node
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
*n = append(*n, node)
|
||||
}
|
||||
|
||||
// SetValueAttribute sets a node's attribute's value or returns false if no node is found.
|
||||
func (n *Nodes) SetValueAttribute(id string, value interface{}) bool {
|
||||
for i := range *n {
|
||||
if (*n)[i].ID() == id {
|
||||
(*n)[i].Attributes.SetValue(value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Append appends a node.
|
||||
func (n *Nodes) Append(node *Node) {
|
||||
*n = append(*n, node)
|
||||
}
|
||||
|
||||
func (n *Nodes) RemoveMatching(node *Node) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r := Nodes{}
|
||||
for k, v := range *n {
|
||||
if !(*n)[k].Matches(node) {
|
||||
r = append(r, v)
|
||||
}
|
||||
}
|
||||
|
||||
*n = r
|
||||
}
|
||||
|
||||
func (n *Node) Matches(needle *Node) bool {
|
||||
if len(needle.ID()) > 0 && n.ID() != needle.ID() {
|
||||
return false
|
||||
}
|
||||
|
||||
if needle.Type != "" && n.Type != needle.Type {
|
||||
return false
|
||||
}
|
||||
|
||||
if needle.Group != "" && n.Group != needle.Group {
|
||||
return false
|
||||
}
|
||||
|
||||
return n.Attributes.Matches(needle.Attributes)
|
||||
}
|
||||
|
||||
func (n *Node) UnmarshalJSON(data []byte) error {
|
||||
var attr Attributes
|
||||
switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) {
|
||||
case Text:
|
||||
attr = &TextAttributes{
|
||||
NodeType: Text,
|
||||
}
|
||||
case Input:
|
||||
attr = &InputAttributes{
|
||||
NodeType: Input,
|
||||
}
|
||||
case Anchor:
|
||||
attr = &AnchorAttributes{
|
||||
NodeType: Anchor,
|
||||
}
|
||||
case Image:
|
||||
attr = &ImageAttributes{
|
||||
NodeType: Image,
|
||||
}
|
||||
case Script:
|
||||
attr = &ScriptAttributes{
|
||||
NodeType: Script,
|
||||
}
|
||||
case Division:
|
||||
attr = &DivisionAttributes{
|
||||
NodeType: Division,
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected node type: %s", t)
|
||||
}
|
||||
|
||||
var d jsonRawNode
|
||||
d.Attributes = attr
|
||||
if err := json.NewDecoder(bytes.NewReader(data)).Decode(&d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*n = Node(d)
|
||||
|
||||
if n.Meta == nil {
|
||||
n.Meta = new(Meta)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) MarshalJSON() ([]byte, error) {
|
||||
var t UiNodeType
|
||||
if n.Attributes != nil {
|
||||
switch attr := n.Attributes.(type) {
|
||||
case *TextAttributes:
|
||||
t = Text
|
||||
attr.NodeType = Text
|
||||
case *InputAttributes:
|
||||
t = Input
|
||||
attr.NodeType = Input
|
||||
case *AnchorAttributes:
|
||||
t = Anchor
|
||||
attr.NodeType = Anchor
|
||||
case *ImageAttributes:
|
||||
t = Image
|
||||
attr.NodeType = Image
|
||||
case *ScriptAttributes:
|
||||
t = Script
|
||||
attr.NodeType = Script
|
||||
case *DivisionAttributes:
|
||||
t = Division
|
||||
attr.NodeType = Division
|
||||
default:
|
||||
return nil, errors.WithStack(fmt.Errorf("unknown node type: %T", n.Attributes))
|
||||
}
|
||||
}
|
||||
|
||||
if n.Type == "" {
|
||||
n.Type = t
|
||||
} else if n.Type != t {
|
||||
return nil, errors.WithStack(fmt.Errorf("node type and node attributes mismatch: %T != %s", n.Attributes, n.Type))
|
||||
}
|
||||
|
||||
if n.Meta == nil {
|
||||
n.Meta = new(Meta)
|
||||
}
|
||||
|
||||
return json.Marshal((*jsonRawNode)(n))
|
||||
}
|
||||
560
ui/node/node_test.go
Normal file
560
ui/node/node_test.go
Normal file
@@ -0,0 +1,560 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package node_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/x/snapshotx"
|
||||
|
||||
"github.com/ory/kratos/text"
|
||||
|
||||
"github.com/ory/x/assertx"
|
||||
|
||||
"github.com/ory/kratos/corpx"
|
||||
|
||||
"github.com/ory/kratos/ui/container"
|
||||
|
||||
"github.com/go-faker/faker/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/kratos/ui/node"
|
||||
)
|
||||
|
||||
func init() {
|
||||
corpx.RegisterFakes()
|
||||
}
|
||||
|
||||
//go:embed fixtures/sort/*
|
||||
var sortFixtures embed.FS
|
||||
var ctx = context.Background()
|
||||
|
||||
func TestNodesSort(t *testing.T) {
|
||||
// use a schema compiler that disables identifiers
|
||||
schemaCompiler := jsonschema.NewCompiler()
|
||||
schemaPath := "fixtures/identity.schema.json"
|
||||
|
||||
f, err := container.NewFromJSONSchema(ctx, "/foo", node.DefaultGroup, schemaPath, "", schemaCompiler, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
f.UpdateNodeValuesFromJSON(json.RawMessage(`{}`), "traits", node.DefaultGroup)
|
||||
f.SetCSRF("csrf_token")
|
||||
|
||||
inputs, err := sortFixtures.ReadDir("fixtures/sort/input")
|
||||
require.NoError(t, err)
|
||||
|
||||
options := map[string][]node.SortOption{
|
||||
"1.json": {
|
||||
node.SortUseOrder([]string{"password_identifier"}),
|
||||
node.SortUpdateOrder(node.PasswordLoginOrder),
|
||||
node.SortByGroups([]node.UiNodeGroup{
|
||||
node.DefaultGroup,
|
||||
node.ProfileGroup,
|
||||
node.OpenIDConnectGroup,
|
||||
node.PasswordGroup,
|
||||
node.LinkGroup,
|
||||
node.LinkGroup,
|
||||
}),
|
||||
},
|
||||
"2.json": {
|
||||
node.SortBySchema(filepath.Join("fixtures/sort/schema", "2.json")),
|
||||
node.SortUpdateOrder(node.PasswordLoginOrder),
|
||||
node.SortByGroups([]node.UiNodeGroup{
|
||||
node.DefaultGroup,
|
||||
node.OpenIDConnectGroup,
|
||||
node.PasswordGroup,
|
||||
}),
|
||||
},
|
||||
"3.json": {
|
||||
node.SortBySchema(filepath.Join("fixtures/sort/schema", "3.json")),
|
||||
node.SortByGroups([]node.UiNodeGroup{
|
||||
node.DefaultGroup,
|
||||
node.OpenIDConnectGroup,
|
||||
node.PasswordGroup,
|
||||
}),
|
||||
},
|
||||
"4.json": {
|
||||
node.SortBySchema(filepath.Join("fixtures/sort/schema", "4.json")),
|
||||
node.SortByGroups([]node.UiNodeGroup{
|
||||
node.DefaultGroup,
|
||||
node.ProfileGroup,
|
||||
node.PasswordGroup,
|
||||
node.OpenIDConnectGroup,
|
||||
node.LookupGroup,
|
||||
node.WebAuthnGroup,
|
||||
node.TOTPGroup,
|
||||
}),
|
||||
node.SortUseOrderAppend([]string{
|
||||
// Lookup
|
||||
node.LookupReveal,
|
||||
node.LookupRegenerate,
|
||||
node.LookupCodes,
|
||||
node.LookupConfirm,
|
||||
|
||||
// Lookup
|
||||
node.WebAuthnRemove,
|
||||
node.WebAuthnRegisterDisplayName,
|
||||
node.WebAuthnRegister,
|
||||
|
||||
// TOTP
|
||||
node.TOTPSecretKey,
|
||||
node.TOTPQR,
|
||||
node.TOTPUnlink,
|
||||
node.TOTPCode,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, in := range inputs {
|
||||
t.Run("file="+in.Name(), func(t *testing.T) {
|
||||
if in.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := sortFixtures.Open(filepath.Join("fixtures/sort/input", in.Name()))
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = fi.Close() }()
|
||||
|
||||
var nodes node.Nodes
|
||||
require.NoError(t, json.NewDecoder(fi).Decode(&nodes))
|
||||
require.NotEmpty(t, nodes)
|
||||
|
||||
require.NoError(t, nodes.SortBySchema(ctx, options[in.Name()]...))
|
||||
|
||||
fe, err := sortFixtures.ReadFile(filepath.Join("fixtures/sort/expected", in.Name()))
|
||||
require.NoError(t, err)
|
||||
|
||||
assertx.EqualAsJSON(t, json.RawMessage(fe), nodes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodesSortStableGroups(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Build count nodes in the same group, sharing an input name (as a list of
|
||||
// remove buttons does). Insertion order is 0..count-1, recorded in each
|
||||
// node's label; the value is the reverse, so a value tiebreak would flip the
|
||||
// order. count exceeds Go's insertion-sort threshold (12) so the sort takes
|
||||
// the quicksort path, where an unstable sort could otherwise reshuffle
|
||||
// same-group nodes.
|
||||
const count = 20
|
||||
build := func() node.Nodes {
|
||||
var n node.Nodes
|
||||
for i := range count {
|
||||
value := fmt.Sprintf("%02d", count-1-i)
|
||||
n = append(n, node.NewInputField(node.DeviceAuthnRemove, value, node.DeviceAuthnGroup, node.InputAttributeTypeSubmit).
|
||||
WithMetaLabel(&text.Message{Text: fmt.Sprintf("%02d", i)}))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
labels := func(n node.Nodes) []string {
|
||||
got := make([]string, 0, len(n))
|
||||
for _, nn := range n {
|
||||
got = append(got, nn.Meta.Label.Text)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
insertionOrder := make([]string, count)
|
||||
valueOrder := make([]string, count)
|
||||
for i := range count {
|
||||
insertionOrder[i] = fmt.Sprintf("%02d", i)
|
||||
valueOrder[i] = fmt.Sprintf("%02d", count-1-i)
|
||||
}
|
||||
|
||||
t.Run("case=stable group preserves insertion order", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
n := build()
|
||||
require.NoError(t, n.SortBySchema(ctx,
|
||||
node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}),
|
||||
node.SortStableGroups(node.DeviceAuthnGroup),
|
||||
))
|
||||
assert.Equal(t, insertionOrder, labels(n))
|
||||
})
|
||||
|
||||
t.Run("case=without the option the value tiebreak reorders", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
n := build()
|
||||
require.NoError(t, n.SortBySchema(ctx,
|
||||
node.SortByGroups([]node.UiNodeGroup{node.DefaultGroup, node.PasswordGroup}),
|
||||
))
|
||||
assert.Equal(t, valueOrder, labels(n))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodesUpsert(t *testing.T) {
|
||||
var nodes node.Nodes
|
||||
nodes.Upsert(node.NewCSRFNode("foo"))
|
||||
require.Len(t, nodes, 1)
|
||||
nodes.Upsert(node.NewCSRFNode("bar"))
|
||||
require.Len(t, nodes, 1)
|
||||
assert.EqualValues(t, "bar", nodes[0].Attributes.GetValue())
|
||||
}
|
||||
|
||||
func TestNodesRemove(t *testing.T) {
|
||||
var nodes node.Nodes
|
||||
nodes.Append(node.NewInputField("other", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
|
||||
nodes.Append(node.NewInputField("link", "foo", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
|
||||
nodes.Append(node.NewInputField("link", "bar", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
|
||||
nodes.Append(node.NewInputField("unlink", "baz", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit))
|
||||
require.Len(t, nodes, 4)
|
||||
|
||||
nodes.Remove("link", "unlink")
|
||||
require.Len(t, nodes, 1)
|
||||
}
|
||||
|
||||
func TestNodesRemoveInGroup(t *testing.T) {
|
||||
samlLink := node.NewInputField("link", "saml-provider", node.SAMLGroup, node.InputAttributeTypeSubmit)
|
||||
nodes := node.Nodes{
|
||||
node.NewInputField("link", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit),
|
||||
samlLink,
|
||||
node.NewInputField("unlink", "oidc-provider", node.OpenIDConnectGroup, node.InputAttributeTypeSubmit),
|
||||
node.NewCSRFNode("token"),
|
||||
}
|
||||
|
||||
// Remove only the OIDC group's link/unlink nodes.
|
||||
nodes.RemoveInGroup(node.OpenIDConnectGroup, "link", "unlink")
|
||||
|
||||
require.Len(t, nodes, 2)
|
||||
// The SAML link node shares the "link" name but a different group: it survives.
|
||||
assert.Contains(t, nodes, samlLink)
|
||||
// The CSRF node (DefaultGroup) is untouched.
|
||||
var sawCSRF bool
|
||||
for _, n := range nodes {
|
||||
if n.Group == node.DefaultGroup {
|
||||
sawCSRF = true
|
||||
}
|
||||
}
|
||||
assert.True(t, sawCSRF, "CSRF node must not be removed")
|
||||
}
|
||||
|
||||
func TestNodeJSON(t *testing.T) {
|
||||
t.Run("idempotent decode", func(t *testing.T) {
|
||||
nodes := make(node.Nodes, 5)
|
||||
for k := range nodes {
|
||||
nodes[k] = new(node.Node)
|
||||
expected := nodes[k]
|
||||
|
||||
require.NoError(t, faker.FakeData(expected))
|
||||
var b bytes.Buffer
|
||||
require.NoError(t, json.NewEncoder(&b).Encode(expected))
|
||||
|
||||
var actual node.Node
|
||||
require.NoError(t, json.NewDecoder(&b).Decode(&actual))
|
||||
|
||||
assert.EqualValues(t, *expected, actual)
|
||||
assert.NotEmpty(t, actual.Type)
|
||||
assert.NotNil(t, actual.Attributes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("type mismatch", func(t *testing.T) {
|
||||
n := &node.Node{Type: node.Image, Attributes: new(node.InputAttributes)}
|
||||
var b bytes.Buffer
|
||||
require.EqualError(t, json.NewEncoder(&b).Encode(n), "json: error calling MarshalJSON for type *node.Node: node type and node attributes mismatch: *node.InputAttributes != img")
|
||||
})
|
||||
|
||||
t.Run("type empty", func(t *testing.T) {
|
||||
n := &node.Node{Attributes: new(node.InputAttributes)}
|
||||
var b bytes.Buffer
|
||||
require.NoError(t, json.NewEncoder(&b).Encode(n))
|
||||
assert.EqualValues(t, node.Input, gjson.GetBytes(b.Bytes(), "type").String())
|
||||
})
|
||||
|
||||
t.Run("type decode unknown", func(t *testing.T) {
|
||||
var n node.Node
|
||||
require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatchesNode(t *testing.T) {
|
||||
// Test when ID is different
|
||||
node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
|
||||
assert.False(t, node1.Matches(node2))
|
||||
|
||||
// Test when Type is different
|
||||
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
assert.False(t, node1.Matches(node2))
|
||||
|
||||
// Test when Group is different
|
||||
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
assert.False(t, node1.Matches(node2))
|
||||
|
||||
// Test when all fields are the same
|
||||
node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
|
||||
assert.True(t, node1.Matches(node2))
|
||||
}
|
||||
|
||||
func TestRemoveMatchingNodes(t *testing.T) {
|
||||
nodes := node.Nodes{
|
||||
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}},
|
||||
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}},
|
||||
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}},
|
||||
}
|
||||
|
||||
// Test when node to remove is present
|
||||
nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
|
||||
nodes.RemoveMatching(nodeToRemove)
|
||||
assert.Len(t, nodes, 2)
|
||||
for _, n := range nodes {
|
||||
assert.NotEqual(t, nodeToRemove.ID(), n.ID())
|
||||
}
|
||||
|
||||
// Test when node to remove is not present
|
||||
nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}}
|
||||
nodes.RemoveMatching(nodeToRemove)
|
||||
assert.Len(t, nodes, 2) // length should remain the same
|
||||
|
||||
// Test when node to remove is present
|
||||
nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}
|
||||
ui := &container.Container{
|
||||
Nodes: nodes,
|
||||
}
|
||||
|
||||
ui.GetNodes().RemoveMatching(nodeToRemove)
|
||||
assert.Len(t, *ui.GetNodes(), 1)
|
||||
for _, n := range *ui.GetNodes() {
|
||||
assert.NotEqual(t, "bar", n.ID())
|
||||
assert.NotEqual(t, "baz", n.ID())
|
||||
}
|
||||
|
||||
ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
|
||||
assert.NotNil(t, ui.Nodes.Find("method"))
|
||||
ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit))
|
||||
assert.Nil(t, ui.Nodes.Find("method"))
|
||||
}
|
||||
|
||||
func TestNodeMarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node *node.Node
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "text node",
|
||||
node: &node.Node{
|
||||
Type: node.Text,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.TextAttributes{
|
||||
NodeType: node.Text,
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "input node",
|
||||
node: &node.Node{
|
||||
Type: node.Input,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.InputAttributes{
|
||||
NodeType: node.Input,
|
||||
Name: "password",
|
||||
Type: "password",
|
||||
FieldValue: "secret",
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchor node",
|
||||
node: &node.Node{
|
||||
Type: node.Anchor,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.AnchorAttributes{
|
||||
NodeType: node.Anchor,
|
||||
HREF: "https://example.com",
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image node",
|
||||
node: &node.Node{
|
||||
Type: node.Image,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.ImageAttributes{
|
||||
NodeType: node.Image,
|
||||
Source: "image.jpg",
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "script node",
|
||||
node: &node.Node{
|
||||
Type: node.Script,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.ScriptAttributes{
|
||||
NodeType: node.Script,
|
||||
Source: "script.js",
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "division node",
|
||||
node: &node.Node{
|
||||
Type: node.Division,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.DivisionAttributes{
|
||||
NodeType: node.Division,
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "type mismatch",
|
||||
node: &node.Node{
|
||||
Type: node.Image,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.InputAttributes{NodeType: node.Input},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "node type and node attributes mismatch",
|
||||
},
|
||||
{
|
||||
name: "empty type inferred from attributes",
|
||||
node: &node.Node{
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: &node.InputAttributes{
|
||||
NodeType: node.Input,
|
||||
Name: "email",
|
||||
},
|
||||
Messages: text.Messages{},
|
||||
Meta: &node.Meta{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
node: &node.Node{
|
||||
Type: node.Image,
|
||||
Group: node.DefaultGroup,
|
||||
Attributes: nil,
|
||||
Messages: text.Messages{},
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "node type and node attributes mismatch",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(tt.node)
|
||||
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use snapshotx for testing serialization
|
||||
snapshotx.SnapshotT(t, json.RawMessage(data))
|
||||
|
||||
// Verify roundtrip
|
||||
var unmarshalled node.Node
|
||||
err = json.Unmarshal(data, &unmarshalled)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Re-marshal for comparison
|
||||
remarshalled, err := json.Marshal(&unmarshalled)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, string(data), string(remarshalled))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodes_FindAll(t *testing.T) {
|
||||
nodes := node.Nodes{
|
||||
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}},
|
||||
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "b"}},
|
||||
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "a"}},
|
||||
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "c"}},
|
||||
}
|
||||
|
||||
t.Run("find matches multiple nodes", func(t *testing.T) {
|
||||
got := nodes.FindAll("a")
|
||||
assert.Equal(t, []node.Node{*nodes[0], *nodes[2]}, got)
|
||||
})
|
||||
|
||||
t.Run("find matches one node", func(t *testing.T) {
|
||||
got := nodes.FindAll("b")
|
||||
assert.Equal(t, []node.Node{*nodes[1]}, got)
|
||||
})
|
||||
|
||||
t.Run("no match returns empty slice", func(t *testing.T) {
|
||||
got := nodes.FindAll("x")
|
||||
assert.Empty(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodes_RemoveGroup(t *testing.T) {
|
||||
nodes := node.Nodes{
|
||||
&node.Node{Type: node.Input, Group: node.DefaultGroup, Attributes: &node.InputAttributes{Name: "default"}},
|
||||
&node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha1"}},
|
||||
&node.Node{Type: node.Input, Group: node.IdentifierFirstGroup, Attributes: &node.InputAttributes{Name: "idfirst"}},
|
||||
&node.Node{Type: node.Input, Group: node.CaptchaGroup, Attributes: &node.InputAttributes{Name: "captcha2"}},
|
||||
&node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "password"}},
|
||||
nil,
|
||||
}
|
||||
|
||||
t.Run("removes single node", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroup(node.IdentifierFirstGroup)
|
||||
assert.Equal(t, node.Nodes{nodes[0], nodes[1], nodes[3], nodes[4]}, n)
|
||||
})
|
||||
|
||||
t.Run("removes multiple nodes", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroup(node.CaptchaGroup)
|
||||
assert.Equal(t, node.Nodes{nodes[0], nodes[2], nodes[4]}, n)
|
||||
})
|
||||
|
||||
t.Run("removes no nodes", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroup(node.PasskeyGroup)
|
||||
assert.Equal(t, nodes[:5], n)
|
||||
})
|
||||
|
||||
t.Run("removes multiple groups", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroups(node.CaptchaGroup, node.PasswordGroup)
|
||||
assert.Equal(t, node.Nodes{nodes[0], nodes[2]}, n)
|
||||
})
|
||||
|
||||
t.Run("removes multiple groups empty", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroups()
|
||||
assert.Equal(t, nodes, n)
|
||||
})
|
||||
|
||||
t.Run("removes multiple groups no matching", func(t *testing.T) {
|
||||
n := append(node.Nodes(nil), nodes...)
|
||||
n.RemoveGroups(node.PasskeyGroup)
|
||||
assert.Equal(t, nodes[:5], n)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user