kratos/ui/node/node.go

489 lines
9.9 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package node
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/text"
"github.com/ory/x/stringslice"
)
// 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
)
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 !stringslice.Has(exclude, nn.ID()) {
nn.Reset()
}
n[k] = nn
}
}
func (n Nodes) ResetNodes(reset ...string) {
for k, nn := range n {
if stringslice.Has(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
}
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
}
}
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, ...
sort.Slice(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
}
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
}
// 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))
}