mirror of
				https://github.com/teamhanko/hanko.git
				synced 2025-10-31 00:27:33 +08:00 
			
		
		
		
	 601ffaae92
			
		
	
	601ffaae92
	
	
	
		
			
			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
 | |
| }
 |