mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-29 23:59:46 +08:00
This pull request introduces the new Flowpilot system along with several new features and various improvements. The key enhancements include configurable authorization, registration, and profile flows, as well as the ability to enable and disable user identifiers (e.g., email addresses and usernames) and login methods. --------- Co-authored-by: Frederic Jahn <frederic.jahn@hanko.io> Co-authored-by: Lennart Fleischmann <lennart.fleischmann@hanko.io> Co-authored-by: lfleischmann <67686424+lfleischmann@users.noreply.github.com> Co-authored-by: merlindru <hello@merlindru.com>
285 lines
8.5 KiB
Go
285 lines
8.5 KiB
Go
package flowpilot
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
type FlowBuilder interface {
|
|
TTL(ttl time.Duration) FlowBuilder
|
|
State(stateName StateName, actions ...Action) FlowBuilder
|
|
InitialState(stateNames ...StateName) FlowBuilder
|
|
ErrorState(stateName StateName) FlowBuilder
|
|
BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder
|
|
AfterState(stateName StateName, hooks ...HookAction) FlowBuilder
|
|
AfterFlow(flowName FlowName, hooks ...HookAction) FlowBuilder
|
|
Debug(enabled bool) FlowBuilder
|
|
SubFlows(subFlows ...subFlow) FlowBuilder
|
|
Build() (Flow, error)
|
|
MustBuild() Flow
|
|
BeforeEachAction(hooks ...HookAction) FlowBuilder
|
|
AfterEachAction(hooks ...HookAction) FlowBuilder
|
|
}
|
|
|
|
// defaultFlowBuilderBase is the base flow builder struct.
|
|
type defaultFlowBuilderBase struct {
|
|
name FlowName
|
|
flow stateActions
|
|
subFlows SubFlows
|
|
stateDetails stateDetails
|
|
beforeStateHooks stateHooks
|
|
afterStateHooks stateHooks
|
|
beforeEachActionHooks hookActions
|
|
afterEachActionHooks hookActions
|
|
afterFlowHooks flowHooks
|
|
}
|
|
|
|
// defaultFlowBuilder is a builder struct for creating a new Flow.
|
|
type defaultFlowBuilder struct {
|
|
path string
|
|
ttl time.Duration
|
|
initialStateNames []StateName
|
|
errorStateName StateName
|
|
debug bool
|
|
|
|
defaultFlowBuilderBase
|
|
}
|
|
|
|
// newFlowBuilderBase creates a new defaultFlowBuilderBase instance.
|
|
func newFlowBuilderBase(name FlowName) defaultFlowBuilderBase {
|
|
return defaultFlowBuilderBase{
|
|
name: name,
|
|
flow: make(stateActions),
|
|
subFlows: make(SubFlows, 0),
|
|
stateDetails: make(stateDetails),
|
|
beforeStateHooks: make(stateHooks),
|
|
afterStateHooks: make(stateHooks),
|
|
afterFlowHooks: make(flowHooks),
|
|
}
|
|
}
|
|
|
|
// NewFlow creates a new defaultFlowBuilder that builds a new flow available under the specified path.
|
|
func NewFlow(name FlowName) FlowBuilder {
|
|
path := fmt.Sprintf("/%s", name)
|
|
fbBase := newFlowBuilderBase(name)
|
|
return &defaultFlowBuilder{path: path, defaultFlowBuilderBase: fbBase}
|
|
}
|
|
|
|
// TTL sets the time-to-live (TTL) for the flow.
|
|
func (fb *defaultFlowBuilder) TTL(ttl time.Duration) FlowBuilder {
|
|
fb.ttl = ttl
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addState(stateName StateName, actions ...Action) {
|
|
fb.flow[stateName] = append(fb.flow[stateName], actions...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addBeforeStateHooks(stateName StateName, hooks ...HookAction) {
|
|
fb.beforeStateHooks[stateName] = append(fb.beforeStateHooks[stateName], hooks...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addAfterStateHooks(stateName StateName, hooks ...HookAction) {
|
|
fb.afterStateHooks[stateName] = append(fb.afterStateHooks[stateName], hooks...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addAfterFlowHooks(flowName FlowName, hooks ...HookAction) {
|
|
fb.afterFlowHooks[flowName] = append(fb.afterFlowHooks[flowName], hooks...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) addBeforeEachActionHooks(hooks ...HookAction) {
|
|
fb.beforeEachActionHooks = append(fb.beforeEachActionHooks, hooks...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) addAfterEachActionHooks(hooks ...HookAction) {
|
|
fb.afterEachActionHooks = append(fb.afterEachActionHooks, hooks...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addSubFlows(subFlows ...subFlow) {
|
|
fb.subFlows = append(fb.subFlows, subFlows...)
|
|
}
|
|
|
|
func (fb *defaultFlowBuilderBase) addStateIfNotExists(stateName StateName) {
|
|
if _, exists := fb.flow[stateName]; !exists {
|
|
fb.addState(stateName)
|
|
}
|
|
}
|
|
|
|
// scanFlowStates iterates through each state in the provided flow and associates relevant information, also it checks
|
|
// for uniqueness of state names.
|
|
func (fb *defaultFlowBuilder) scanFlowStates(flow flowBase) error {
|
|
// Iterate through states in the flow.
|
|
for stateName, actions := range flow.getFlow() {
|
|
// Check if state name is already in use.
|
|
if _, ok := fb.stateDetails[stateName]; ok {
|
|
continue
|
|
}
|
|
|
|
actionDetails := make(defaultActionDetails, len(actions))
|
|
|
|
for i, action := range actions {
|
|
actionDetails[i] = &defaultActionDetail{
|
|
action: action,
|
|
flowName: flow.getName(),
|
|
}
|
|
}
|
|
|
|
// Create state details.
|
|
state := &defaultStateDetail{
|
|
name: stateName,
|
|
actionDetails: actionDetails,
|
|
flow: flow.getFlow(),
|
|
subFlows: flow.getSubFlows(),
|
|
flowName: flow.getName(),
|
|
}
|
|
|
|
// Store state details.
|
|
fb.stateDetails[stateName] = state
|
|
}
|
|
|
|
for stateName, actions := range flow.getBeforeStateHooks() {
|
|
fb.beforeStateHooks[stateName] = append(fb.beforeStateHooks[stateName], actions...)
|
|
}
|
|
|
|
for stateName, actions := range flow.getAfterStateHooks() {
|
|
fb.afterStateHooks[stateName] = append(fb.afterStateHooks[stateName], actions...)
|
|
}
|
|
|
|
actions := flow.getAfterFlowHooks()
|
|
fb.afterFlowHooks[flow.getName()] = append(fb.afterFlowHooks[flow.getName()], actions...)
|
|
|
|
// Recursively scan sub-flows.
|
|
for _, sf := range flow.getSubFlows() {
|
|
if err := fb.scanFlowStates(sf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validate performs validation checks on the flow configuration.
|
|
func (fb *defaultFlowBuilder) validate() error {
|
|
// Validate fixed states and their presence in the flow.
|
|
if len(fb.initialStateNames) == 0 {
|
|
return errors.New("fixed state 'initialState' is not set")
|
|
}
|
|
if len(fb.errorStateName) == 0 {
|
|
return errors.New("fixed state 'errorState' is not set")
|
|
}
|
|
if !fb.flow.stateExists(fb.initialStateNames[0]) && !fb.subFlows.stateExists(fb.initialStateNames[0]) {
|
|
return fmt.Errorf("initial state '%s' does not belong to the flow or a sub-flow", fb.initialStateNames[0])
|
|
}
|
|
if !fb.flow.stateExists(fb.errorStateName) {
|
|
return fmt.Errorf("error state '%s' does not belong to the flow", fb.errorStateName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// State adds a new state to the flow.
|
|
func (fb *defaultFlowBuilder) State(stateName StateName, actions ...Action) FlowBuilder {
|
|
fb.addState(stateName, actions...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) BeforeState(stateName StateName, hooks ...HookAction) FlowBuilder {
|
|
fb.addBeforeStateHooks(stateName, hooks...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) AfterState(stateName StateName, hooks ...HookAction) FlowBuilder {
|
|
fb.addAfterStateHooks(stateName, hooks...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) AfterFlow(flowName FlowName, hooks ...HookAction) FlowBuilder {
|
|
fb.addAfterFlowHooks(flowName, hooks...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) BeforeEachAction(hooks ...HookAction) FlowBuilder {
|
|
fb.addBeforeEachActionHooks(hooks...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) AfterEachAction(hooks ...HookAction) FlowBuilder {
|
|
fb.addAfterEachActionHooks(hooks...)
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) InitialState(nextStateNames ...StateName) FlowBuilder {
|
|
fb.initialStateNames = nextStateNames
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) ErrorState(stateName StateName) FlowBuilder {
|
|
fb.addStateIfNotExists(stateName)
|
|
fb.errorStateName = stateName
|
|
return fb
|
|
}
|
|
|
|
func (fb *defaultFlowBuilder) SubFlows(subFlows ...subFlow) FlowBuilder {
|
|
fb.addSubFlows(subFlows...)
|
|
return fb
|
|
}
|
|
|
|
// Debug enables the debug mode, which causes the flow response to contain the actual error.
|
|
func (fb *defaultFlowBuilder) Debug(enabled bool) FlowBuilder {
|
|
fb.debug = enabled
|
|
return fb
|
|
}
|
|
|
|
// Build constructs and returns the Flow object.
|
|
func (fb *defaultFlowBuilder) Build() (Flow, error) {
|
|
if err := fb.validate(); err != nil {
|
|
return nil, fmt.Errorf("flow validation failed: %w", err)
|
|
}
|
|
|
|
dfb := &defaultFlowBase{
|
|
name: fb.name,
|
|
flow: fb.flow,
|
|
subFlows: fb.subFlows,
|
|
beforeStateHooks: fb.beforeStateHooks,
|
|
afterStateHooks: fb.afterStateHooks,
|
|
beforeEachActionHooks: fb.beforeEachActionHooks,
|
|
afterEachActionHooks: fb.afterEachActionHooks,
|
|
afterFlowHooks: fb.afterFlowHooks,
|
|
}
|
|
|
|
flow := &defaultFlow{
|
|
initialStateNames: fb.initialStateNames,
|
|
errorStateName: fb.errorStateName,
|
|
stateDetails: fb.stateDetails,
|
|
ttl: fb.ttl,
|
|
debug: fb.debug,
|
|
defaultFlowBase: dfb,
|
|
contextValues: make(contextValues),
|
|
}
|
|
|
|
// Check if states were already scanned, if so, don't scan again
|
|
if len(fb.stateDetails) == 0 {
|
|
if err := fb.scanFlowStates(flow); err != nil {
|
|
return nil, fmt.Errorf("failed to scan flow states: %w", err)
|
|
}
|
|
}
|
|
|
|
flow.defaultFlowBase.beforeStateHooks.makeUnique()
|
|
flow.defaultFlowBase.afterStateHooks.makeUnique()
|
|
flow.defaultFlowBase.afterFlowHooks.makeUnique()
|
|
|
|
return flow, nil
|
|
}
|
|
|
|
// MustBuild constructs and returns the Flow object, panics on error.
|
|
func (fb *defaultFlowBuilder) MustBuild() Flow {
|
|
f, err := fb.Build()
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return f
|
|
}
|